From a10c3ad0feb4e8967cc450940cadcd19d129353f Mon Sep 17 00:00:00 2001 From: mleku Date: Wed, 3 Dec 2025 08:28:59 +0000 Subject: [PATCH] update to latest gioui.org API, fix cross platform builds --- .claude/settings.local.json | 27 + app.go | 11 +- bool.go | 2 +- border.go | 8 +- button.go | 18 +- buttonlayout.go | 8 +- card.go | 2 +- checkable.go | 12 +- checkbox.go | 4 +- clickable.go | 8 +- clipboard/log.go | 4 +- cmd/clipboard/logmain.go | 4 +- cmd/clipboard/main.go | 8 +- cmd/hello/logmain.go | 4 +- cmd/hello/main.go | 6 +- cmd/iconchooser/logmain.go | 4 +- cmd/iconchooser/main.go | 10 +- column.go | 6 +- dialog/dialog.go_ | 10 +- dialog/example/main.go_ | 12 +- dimensionlist.go | 4 +- direction.go | 2 +- editor.go | 18 +- editor_test.go_ | 16 +- enum.go | 6 +- fill.go | 6 +- flex.go | 2 +- float.go | 8 +- fonts/bariolbold/data.go | 2 +- fonts/bariolbolditalic/data.go | 2 +- fonts/bariollight/data.go | 2 +- fonts/bariollightitalic/data.go | 2 +- fonts/bariolregular/data.go | 2 +- fonts/bariolregularitalic/data.go | 2 +- fonts/bariolthin/data.go | 2 +- fonts/bariolthinitalic/data.go | 2 +- fonts/logmain.go | 4 +- fonts/main.go | 10 +- fonts/p9fonts/fonts.go | 18 +- fonts/plan9/data.go | 2 +- gio/.builds/apple.yml | 74 - gio/.builds/freebsd.yml | 22 - gio/.builds/linux.yml | 90 - gio/.builds/openbsd.yml | 18 - gio/LICENSE | 63 - gio/LOCAL_MODIFICATIONS.md | 50 - gio/README.md | 35 - gio/app/app.go | 149 -- gio/app/d3d11_windows.go | 135 -- gio/app/datadir.go | 12 - gio/app/doc.go | 66 - gio/app/egl_android.go | 66 - gio/app/egl_wayland.go | 88 - gio/app/egl_windows.go | 59 - gio/app/egl_x11.go | 61 - gio/app/framework_ios.h | 6 - gio/app/gl_ios.go | 148 -- gio/app/gl_js.go | 79 - gio/app/gl_macos.go | 123 - gio/app/ime.go | 145 -- gio/app/ime_test.go | 166 -- gio/app/internal/windows/windows.go | 960 -------- gio/app/internal/xkb/xkb_unix.go | 375 --- gio/app/log_android.go | 87 - gio/app/log_ios.go | 54 - gio/app/log_windows.go | 35 - gio/app/metal_darwin.go | 174 -- gio/app/metal_ios.go | 51 - gio/app/metal_macos.go | 51 - gio/app/os.go | 365 --- gio/app/os_android.go | 1500 ------------ gio/app/os_darwin.go | 265 -- gio/app/os_ios.go | 446 ---- gio/app/os_js.go | 827 ------- gio/app/os_macos.go | 1172 --------- gio/app/os_unix.go | 99 - gio/app/os_wayland.c | 124 - gio/app/os_wayland.go | 1945 --------------- gio/app/os_windows.go | 1054 -------- gio/app/os_x11.go | 928 ------- gio/app/permission/bluetooth/main.go | 24 - gio/app/permission/camera/main.go | 16 - gio/app/permission/doc.go | 50 - gio/app/permission/networkstate/main.go | 12 - gio/app/permission/storage/main.go | 17 - gio/app/permission/wakelock/wakelock.go | 13 - gio/app/runmain.go | 30 - gio/app/system.go | 13 - gio/app/vulkan.go | 218 -- gio/app/vulkan_android.go | 90 - gio/app/vulkan_wayland.go | 81 - gio/app/vulkan_x11.go | 81 - gio/app/wayland_text_input.c | 100 - gio/app/wayland_text_input.h | 836 ------- gio/app/wayland_xdg_decoration.c | 79 - gio/app/wayland_xdg_decoration.h | 382 --- gio/app/wayland_xdg_shell.c | 185 -- gio/app/wayland_xdg_shell.h | 2003 --------------- gio/app/window.go | 981 -------- gio/f32/affine.go | 181 -- gio/f32/affine_test.go | 385 --- gio/f32/f32.go | 60 - gio/flake.lock | 61 - gio/flake.nix | 54 - gio/font/font.go | 126 - gio/font/gofont/gofont.go | 83 - gio/font/opentype/opentype.go | 190 -- gio/gesture/gesture.go | 488 ---- gio/gesture/gesture_test.go | 118 - gio/gpu/api.go | 40 - gio/gpu/caches.go | 152 -- gio/gpu/clip.go | 146 -- gio/gpu/clip_test.go | 21 - gio/gpu/gpu.go | 1603 ------------ gio/gpu/headless/driver_test.go | 208 -- gio/gpu/headless/headless.go | 157 -- gio/gpu/headless/headless_darwin.go | 72 - gio/gpu/headless/headless_egl.go | 16 - gio/gpu/headless/headless_js.go | 47 - gio/gpu/headless/headless_test.go | 149 -- gio/gpu/headless/headless_vulkan.go | 74 - gio/gpu/headless/headless_windows.go | 45 - gio/gpu/internal/d3d11/d3d11.go | 5 - gio/gpu/internal/d3d11/d3d11_windows.go | 871 ------- gio/gpu/internal/driver/api.go | 129 - gio/gpu/internal/driver/driver.go | 240 -- gio/gpu/internal/metal/metal.go | 5 - gio/gpu/internal/metal/metal_darwin.go | 1159 --------- gio/gpu/internal/opengl/opengl.go | 1374 ----------- gio/gpu/internal/opengl/srgb.go | 176 -- gio/gpu/internal/rendertest/bench_test.go | 309 --- gio/gpu/internal/rendertest/clip_test.go | 326 --- gio/gpu/internal/rendertest/doc.go | 4 - .../rendertest/refs/TestBuildOffscreen.png | Bin 112 -> 0 bytes .../rendertest/refs/TestBuildOffscreen_1.png | Bin 413 -> 0 bytes .../rendertest/refs/TestClipOffset.png | Bin 183 -> 0 bytes .../rendertest/refs/TestClipPaintOffset.png | Bin 173 -> 0 bytes .../rendertest/refs/TestClipRotate.png | Bin 232 -> 0 bytes .../rendertest/refs/TestClipScale.png | Bin 183 -> 0 bytes .../refs/TestComplicatedTransform.png | Bin 569 -> 0 bytes .../rendertest/refs/TestDeferredPaint.png | Bin 487 -> 0 bytes .../rendertest/refs/TestDepthOverlap.png | Bin 303 -> 0 bytes .../refs/TestGapsInPath/Outline.png | Bin 408 -> 0 bytes .../rendertest/refs/TestGapsInPath/Stroke.png | Bin 488 -> 0 bytes .../rendertest/refs/TestImageRGBA.png | Bin 395 -> 0 bytes .../refs/TestImageRGBA_ScaleLinear.png | Bin 2986 -> 0 bytes .../refs/TestImageRGBA_ScaleNearest.png | Bin 379 -> 0 bytes .../rendertest/refs/TestInstancedRects.png | Bin 787 -> 0 bytes .../refs/TestLinearGradientAngled.png | Bin 1646 -> 0 bytes .../rendertest/refs/TestNegativeOverlaps.png | Bin 112 -> 0 bytes .../rendertest/refs/TestNoClipFromPaint.png | Bin 163 -> 0 bytes .../refs/TestOffsetScaleTexture.png | Bin 402 -> 0 bytes .../rendertest/refs/TestOffsetTexture.png | Bin 386 -> 0 bytes .../internal/rendertest/refs/TestOpacity.png | Bin 1993 -> 0 bytes .../rendertest/refs/TestPaintAbsolute.png | Bin 651 -> 0 bytes .../internal/rendertest/refs/TestPaintArc.png | Bin 1747 -> 0 bytes .../refs/TestPaintClippedCircle.png | Bin 276 -> 0 bytes .../rendertest/refs/TestPaintClippedRect.png | Bin 189 -> 0 bytes .../refs/TestPaintClippedRectOffset.png | Bin 435 -> 0 bytes .../refs/TestPaintClippedTexture.png | Bin 304 -> 0 bytes .../rendertest/refs/TestPaintOffset.png | Bin 216 -> 0 bytes .../rendertest/refs/TestPaintRect.png | Bin 211 -> 0 bytes .../rendertest/refs/TestPaintRotate.png | Bin 871 -> 0 bytes .../rendertest/refs/TestPaintShear.png | Bin 256 -> 0 bytes .../rendertest/refs/TestPaintTexture.png | Bin 383 -> 0 bytes .../rendertest/refs/TestPathReuse.png | Bin 1309 -> 0 bytes .../rendertest/refs/TestRepeatedPaintsZ.png | Bin 216 -> 0 bytes .../rendertest/refs/TestReuseStencil.png | Bin 141 -> 0 bytes .../rendertest/refs/TestRotateClipTexture.png | Bin 791 -> 0 bytes .../rendertest/refs/TestRotateTexture.png | Bin 1026 -> 0 bytes .../refs/TestStrokedPathBalloon.png | Bin 1977 -> 0 bytes .../TestStrokedPathCoincidentControlPoint.png | Bin 1756 -> 0 bytes .../refs/TestStrokedPathZeroWidth.png | Bin 119 -> 0 bytes .../rendertest/refs/TestStrokedRect.png | Bin 584 -> 0 bytes .../rendertest/refs/TestTexturedStroke.png | Bin 692 -> 0 bytes .../refs/TestTexturedStrokeClipped.png | Bin 692 -> 0 bytes .../rendertest/refs/TestTransformMacro.png | Bin 219 -> 0 bytes .../rendertest/refs/TestTransformOrder.png | Bin 252 -> 0 bytes gio/gpu/internal/rendertest/render_test.go | 506 ---- gio/gpu/internal/rendertest/transform_test.go | 201 -- gio/gpu/internal/rendertest/util_test.go | 306 --- gio/gpu/internal/vulkan/vulkan.go | 1163 --------- gio/gpu/internal/vulkan/vulkan_nosupport.go | 5 - gio/gpu/pack.go | 109 - gio/gpu/pack_test.go | 30 - gio/gpu/path.go | 424 ---- gio/gpu/timer.go | 94 - gio/internal/byteslice/byteslice.go | 36 - gio/internal/cocoainit/cocoa_darwin.go | 21 - gio/internal/d3d11/d3d11_windows.go | 1694 ------------- gio/internal/debug/debug.go | 57 - gio/internal/egl/egl.go | 247 -- gio/internal/egl/egl_unix.go | 109 - gio/internal/egl/egl_windows.go | 191 -- gio/internal/f32/f32.go | 177 -- gio/internal/f32color/f32colorgen/main.go | 59 - gio/internal/f32color/rgba.go | 191 -- gio/internal/f32color/rgba_test.go | 58 - gio/internal/f32color/tables.go | 25 - gio/internal/fling/animation.go | 95 - gio/internal/fling/extrapolation.go | 332 --- gio/internal/fling/extrapolation_test.go | 43 - gio/internal/gl/gl.go | 131 - gio/internal/gl/gl_js.go | 748 ------ gio/internal/gl/gl_unix.go | 1323 ---------- gio/internal/gl/gl_windows.go | 721 ------ gio/internal/gl/types.go | 77 - gio/internal/gl/types_js.go | 90 - gio/internal/gl/util.go | 87 - gio/internal/ops/ops.go | 497 ---- gio/internal/ops/reader.go | 190 -- gio/internal/scene/scene.go | 251 -- gio/internal/stroke/stroke.go | 760 ------ gio/internal/stroke/stroke_test.go | 171 -- gio/internal/vk/vulkan.go | 2145 ----------------- gio/internal/vk/vulkan_android.go | 46 - gio/internal/vk/vulkan_wayland.go | 49 - gio/internal/vk/vulkan_x11.go | 47 - gio/io/clipboard/clipboard.go | 24 - gio/io/event/event.go | 33 - gio/io/input/clipboard.go | 71 - gio/io/input/clipboard_test.go | 125 - gio/io/input/doc.go | 15 - gio/io/input/key.go | 364 --- gio/io/input/key_test.go | 332 --- gio/io/input/pointer.go | 1008 -------- gio/io/input/pointer_test.go | 1347 ----------- gio/io/input/router.go | 893 ------- gio/io/input/router_test.go | 34 - gio/io/input/semantic_test.go | 135 -- gio/io/key/key.go | 289 --- gio/io/key/mod.go | 14 - gio/io/key/mod_darwin.go | 11 - gio/io/pointer/doc.go | 118 - gio/io/pointer/pointer.go | 407 ---- gio/io/pointer/pointer_test.go | 33 - gio/io/semantic/semantic.go | 91 - gio/io/system/decoration.go | 77 - gio/io/system/locale.go | 68 - gio/io/transfer/transfer.go | 95 - gio/layout/alloc_test.go | 49 - gio/layout/context.go | 53 - gio/layout/doc.go | 49 - gio/layout/example_test.go | 158 -- gio/layout/flex.go | 250 -- gio/layout/layout.go | 344 --- gio/layout/layout_test.go | 144 -- gio/layout/list.go | 396 --- gio/layout/list_test.go | 203 -- gio/layout/stack.go | 163 -- gio/layout/stack_test.go | 64 - gio/op/clip/clip.go | 354 --- gio/op/clip/clip_internal_test.go | 106 - gio/op/clip/clip_test.go | 81 - gio/op/clip/doc.go | 14 - gio/op/clip/shapes.go | 175 -- gio/op/clip/shapes_test.go | 18 - gio/op/op.go | 230 -- gio/op/op_test.go | 36 - gio/op/paint/doc.go | 15 - gio/op/paint/paint.go | 195 -- gio/shader/LICENSE | 63 - gio/shader/README.md | 27 - gio/shader/flake.lock | 61 - gio/shader/flake.nix | 21 - gio/shader/gio/common.h | 35 - gio/shader/gio/gen.go | 5 - gio/shader/gio/shaders.go | 796 ------ gio/shader/shader.go | 65 - gio/text/family_parser.go | 246 -- gio/text/family_parser_test.go | 178 -- gio/text/gotext.go | 913 ------- gio/text/gotext_test.go | 595 ----- gio/text/lru.go | 183 -- gio/text/lru_test.go | 55 - gio/text/shaper.go | 616 ----- gio/text/shaper_test.go | 583 ----- gio/text/text.go | 56 - gio/unit/unit.go | 78 - gio/unit/unit_test.go | 48 - gio/widget/bool.go | 48 - gio/widget/border.go | 44 - gio/widget/buffer.go | 128 - gio/widget/button.go | 187 -- gio/widget/button_test.go | 94 - gio/widget/decorations.go | 63 - gio/widget/dnd.go | 92 - gio/widget/dnd_test.go | 94 - gio/widget/doc.go | 6 - gio/widget/editor.go | 1116 --------- gio/widget/editor_test.go | 1285 ---------- gio/widget/enum.go | 139 -- gio/widget/example_test.go | 154 -- gio/widget/fit.go | 95 - gio/widget/fit_test.go | 99 - gio/widget/float.go | 71 - gio/widget/icon.go | 72 - gio/widget/icon_test.go | 74 - gio/widget/image.go | 54 - gio/widget/image_test.go | 66 - gio/widget/index.go | 510 ---- gio/widget/index_test.go | 895 ------- gio/widget/label.go | 226 -- gio/widget/label_test.go | 229 -- gio/widget/list.go | 203 -- gio/widget/material/button.go | 297 --- gio/widget/material/checkable.go | 85 - gio/widget/material/checkbox.go | 40 - gio/widget/material/decorations.go | 209 -- gio/widget/material/doc.go | 59 - gio/widget/material/editor.go | 102 - gio/widget/material/label.go | 154 -- gio/widget/material/list.go | 323 --- gio/widget/material/list_test.go | 67 - gio/widget/material/loader.go | 79 - gio/widget/material/progressbar.go | 74 - gio/widget/material/progresscircle.go | 47 - gio/widget/material/radiobutton.go | 49 - gio/widget/material/slider.go | 96 - gio/widget/material/switch.go | 134 - gio/widget/material/theme.go | 95 - gio/widget/selectable.go | 391 --- gio/widget/selectable_test.go | 121 - gio/widget/text.go | 852 ------- gio/widget/text_bench_test.go | 353 --- gio/widget/widget_test.go | 62 - glyphiter.go | 12 +- glyphiter_test.go | 2 +- go.mod | 20 + helpers.go | 34 +- icon.go | 6 +- iconbutton.go | 8 +- icons/icongen/logmain.go | 4 +- icons/icongen/main.go | 4 +- image.go | 8 +- incdec.go | 4 +- indefinite.go | 12 +- input.go | 2 +- inset.go | 4 +- intslider.go | 46 +- label.go | 14 +- list.go | 16 +- log.go | 4 +- multi.go | 4 +- password.go | 4 +- poolgen/log.go | 4 +- poolgen/poolgen.go | 2 +- pooltypes.go | 8 +- progressbar.go | 10 +- radiobutton.go | 6 +- responsive.go | 4 +- slider.go | 12 +- stack.go | 2 +- switch.go | 12 +- table.go | 10 +- testutil_test.go | 12 +- text.go | 12 +- textinput.go | 14 +- texttable.go | 4 +- theme.go | 10 +- toast/example/main.go_ | 12 +- toast/toast.go_ | 12 +- window.go | 38 +- wraplist.go | 2 +- 364 files changed, 363 insertions(+), 63307 deletions(-) create mode 100644 .claude/settings.local.json delete mode 100644 gio/.builds/apple.yml delete mode 100644 gio/.builds/freebsd.yml delete mode 100644 gio/.builds/linux.yml delete mode 100644 gio/.builds/openbsd.yml delete mode 100644 gio/LICENSE delete mode 100644 gio/LOCAL_MODIFICATIONS.md delete mode 100644 gio/README.md delete mode 100644 gio/app/app.go delete mode 100644 gio/app/d3d11_windows.go delete mode 100644 gio/app/datadir.go delete mode 100644 gio/app/doc.go delete mode 100644 gio/app/egl_android.go delete mode 100644 gio/app/egl_wayland.go delete mode 100644 gio/app/egl_windows.go delete mode 100644 gio/app/egl_x11.go delete mode 100644 gio/app/framework_ios.h delete mode 100644 gio/app/gl_ios.go delete mode 100644 gio/app/gl_js.go delete mode 100644 gio/app/gl_macos.go delete mode 100644 gio/app/ime.go delete mode 100644 gio/app/ime_test.go delete mode 100644 gio/app/internal/windows/windows.go delete mode 100644 gio/app/internal/xkb/xkb_unix.go delete mode 100644 gio/app/log_android.go delete mode 100644 gio/app/log_ios.go delete mode 100644 gio/app/log_windows.go delete mode 100644 gio/app/metal_darwin.go delete mode 100644 gio/app/metal_ios.go delete mode 100644 gio/app/metal_macos.go delete mode 100644 gio/app/os.go delete mode 100644 gio/app/os_android.go delete mode 100644 gio/app/os_darwin.go delete mode 100644 gio/app/os_ios.go delete mode 100644 gio/app/os_js.go delete mode 100644 gio/app/os_macos.go delete mode 100644 gio/app/os_unix.go delete mode 100644 gio/app/os_wayland.c delete mode 100644 gio/app/os_wayland.go delete mode 100644 gio/app/os_windows.go delete mode 100644 gio/app/os_x11.go delete mode 100644 gio/app/permission/bluetooth/main.go delete mode 100644 gio/app/permission/camera/main.go delete mode 100644 gio/app/permission/doc.go delete mode 100644 gio/app/permission/networkstate/main.go delete mode 100644 gio/app/permission/storage/main.go delete mode 100644 gio/app/permission/wakelock/wakelock.go delete mode 100644 gio/app/runmain.go delete mode 100644 gio/app/system.go delete mode 100644 gio/app/vulkan.go delete mode 100644 gio/app/vulkan_android.go delete mode 100644 gio/app/vulkan_wayland.go delete mode 100644 gio/app/vulkan_x11.go delete mode 100644 gio/app/wayland_text_input.c delete mode 100644 gio/app/wayland_text_input.h delete mode 100644 gio/app/wayland_xdg_decoration.c delete mode 100644 gio/app/wayland_xdg_decoration.h delete mode 100644 gio/app/wayland_xdg_shell.c delete mode 100644 gio/app/wayland_xdg_shell.h delete mode 100644 gio/app/window.go delete mode 100644 gio/f32/affine.go delete mode 100644 gio/f32/affine_test.go delete mode 100644 gio/f32/f32.go delete mode 100644 gio/flake.lock delete mode 100644 gio/flake.nix delete mode 100644 gio/font/font.go delete mode 100644 gio/font/gofont/gofont.go delete mode 100644 gio/font/opentype/opentype.go delete mode 100644 gio/gesture/gesture.go delete mode 100644 gio/gesture/gesture_test.go delete mode 100644 gio/gpu/api.go delete mode 100644 gio/gpu/caches.go delete mode 100644 gio/gpu/clip.go delete mode 100644 gio/gpu/clip_test.go delete mode 100644 gio/gpu/gpu.go delete mode 100644 gio/gpu/headless/driver_test.go delete mode 100644 gio/gpu/headless/headless.go delete mode 100644 gio/gpu/headless/headless_darwin.go delete mode 100644 gio/gpu/headless/headless_egl.go delete mode 100644 gio/gpu/headless/headless_js.go delete mode 100644 gio/gpu/headless/headless_test.go delete mode 100644 gio/gpu/headless/headless_vulkan.go delete mode 100644 gio/gpu/headless/headless_windows.go delete mode 100644 gio/gpu/internal/d3d11/d3d11.go delete mode 100644 gio/gpu/internal/d3d11/d3d11_windows.go delete mode 100644 gio/gpu/internal/driver/api.go delete mode 100644 gio/gpu/internal/driver/driver.go delete mode 100644 gio/gpu/internal/metal/metal.go delete mode 100644 gio/gpu/internal/metal/metal_darwin.go delete mode 100644 gio/gpu/internal/opengl/opengl.go delete mode 100644 gio/gpu/internal/opengl/srgb.go delete mode 100644 gio/gpu/internal/rendertest/bench_test.go delete mode 100644 gio/gpu/internal/rendertest/clip_test.go delete mode 100644 gio/gpu/internal/rendertest/doc.go delete mode 100644 gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestClipOffset.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestClipRotate.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestClipScale.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestDeferredPaint.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestDepthOverlap.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestGapsInPath/Outline.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestGapsInPath/Stroke.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestImageRGBA.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestImageRGBA_ScaleLinear.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestImageRGBA_ScaleNearest.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestInstancedRects.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestLinearGradientAngled.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestOffsetTexture.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestOpacity.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintArc.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintClippedRectOffset.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintOffset.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintRect.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintRotate.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintShear.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPaintTexture.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestPathReuse.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestRepeatedPaintsZ.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestReuseStencil.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestRotateClipTexture.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestRotateTexture.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestStrokedPathBalloon.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestStrokedPathCoincidentControlPoint.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestStrokedPathZeroWidth.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestStrokedRect.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestTexturedStroke.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestTransformMacro.png delete mode 100644 gio/gpu/internal/rendertest/refs/TestTransformOrder.png delete mode 100644 gio/gpu/internal/rendertest/render_test.go delete mode 100644 gio/gpu/internal/rendertest/transform_test.go delete mode 100644 gio/gpu/internal/rendertest/util_test.go delete mode 100644 gio/gpu/internal/vulkan/vulkan.go delete mode 100644 gio/gpu/internal/vulkan/vulkan_nosupport.go delete mode 100644 gio/gpu/pack.go delete mode 100644 gio/gpu/pack_test.go delete mode 100644 gio/gpu/path.go delete mode 100644 gio/gpu/timer.go delete mode 100644 gio/internal/byteslice/byteslice.go delete mode 100644 gio/internal/cocoainit/cocoa_darwin.go delete mode 100644 gio/internal/d3d11/d3d11_windows.go delete mode 100644 gio/internal/debug/debug.go delete mode 100644 gio/internal/egl/egl.go delete mode 100644 gio/internal/egl/egl_unix.go delete mode 100644 gio/internal/egl/egl_windows.go delete mode 100644 gio/internal/f32/f32.go delete mode 100644 gio/internal/f32color/f32colorgen/main.go delete mode 100644 gio/internal/f32color/rgba.go delete mode 100644 gio/internal/f32color/rgba_test.go delete mode 100644 gio/internal/f32color/tables.go delete mode 100644 gio/internal/fling/animation.go delete mode 100644 gio/internal/fling/extrapolation.go delete mode 100644 gio/internal/fling/extrapolation_test.go delete mode 100644 gio/internal/gl/gl.go delete mode 100644 gio/internal/gl/gl_js.go delete mode 100644 gio/internal/gl/gl_unix.go delete mode 100644 gio/internal/gl/gl_windows.go delete mode 100644 gio/internal/gl/types.go delete mode 100644 gio/internal/gl/types_js.go delete mode 100644 gio/internal/gl/util.go delete mode 100644 gio/internal/ops/ops.go delete mode 100644 gio/internal/ops/reader.go delete mode 100644 gio/internal/scene/scene.go delete mode 100644 gio/internal/stroke/stroke.go delete mode 100644 gio/internal/stroke/stroke_test.go delete mode 100644 gio/internal/vk/vulkan.go delete mode 100644 gio/internal/vk/vulkan_android.go delete mode 100644 gio/internal/vk/vulkan_wayland.go delete mode 100644 gio/internal/vk/vulkan_x11.go delete mode 100644 gio/io/clipboard/clipboard.go delete mode 100644 gio/io/event/event.go delete mode 100644 gio/io/input/clipboard.go delete mode 100644 gio/io/input/clipboard_test.go delete mode 100644 gio/io/input/doc.go delete mode 100644 gio/io/input/key.go delete mode 100644 gio/io/input/key_test.go delete mode 100644 gio/io/input/pointer.go delete mode 100644 gio/io/input/pointer_test.go delete mode 100644 gio/io/input/router.go delete mode 100644 gio/io/input/router_test.go delete mode 100644 gio/io/input/semantic_test.go delete mode 100644 gio/io/key/key.go delete mode 100644 gio/io/key/mod.go delete mode 100644 gio/io/key/mod_darwin.go delete mode 100644 gio/io/pointer/doc.go delete mode 100644 gio/io/pointer/pointer.go delete mode 100644 gio/io/pointer/pointer_test.go delete mode 100644 gio/io/semantic/semantic.go delete mode 100644 gio/io/system/decoration.go delete mode 100644 gio/io/system/locale.go delete mode 100644 gio/io/transfer/transfer.go delete mode 100644 gio/layout/alloc_test.go delete mode 100644 gio/layout/context.go delete mode 100644 gio/layout/doc.go delete mode 100644 gio/layout/example_test.go delete mode 100644 gio/layout/flex.go delete mode 100644 gio/layout/layout.go delete mode 100644 gio/layout/layout_test.go delete mode 100644 gio/layout/list.go delete mode 100644 gio/layout/list_test.go delete mode 100644 gio/layout/stack.go delete mode 100644 gio/layout/stack_test.go delete mode 100644 gio/op/clip/clip.go delete mode 100644 gio/op/clip/clip_internal_test.go delete mode 100644 gio/op/clip/clip_test.go delete mode 100644 gio/op/clip/doc.go delete mode 100644 gio/op/clip/shapes.go delete mode 100644 gio/op/clip/shapes_test.go delete mode 100644 gio/op/op.go delete mode 100644 gio/op/op_test.go delete mode 100644 gio/op/paint/doc.go delete mode 100644 gio/op/paint/paint.go delete mode 100644 gio/shader/LICENSE delete mode 100644 gio/shader/README.md delete mode 100644 gio/shader/flake.lock delete mode 100644 gio/shader/flake.nix delete mode 100644 gio/shader/gio/common.h delete mode 100644 gio/shader/gio/gen.go delete mode 100644 gio/shader/gio/shaders.go delete mode 100644 gio/shader/shader.go delete mode 100644 gio/text/family_parser.go delete mode 100644 gio/text/family_parser_test.go delete mode 100644 gio/text/gotext.go delete mode 100644 gio/text/gotext_test.go delete mode 100644 gio/text/lru.go delete mode 100644 gio/text/lru_test.go delete mode 100644 gio/text/shaper.go delete mode 100644 gio/text/shaper_test.go delete mode 100644 gio/text/text.go delete mode 100644 gio/unit/unit.go delete mode 100644 gio/unit/unit_test.go delete mode 100644 gio/widget/bool.go delete mode 100644 gio/widget/border.go delete mode 100644 gio/widget/buffer.go delete mode 100644 gio/widget/button.go delete mode 100644 gio/widget/button_test.go delete mode 100644 gio/widget/decorations.go delete mode 100644 gio/widget/dnd.go delete mode 100644 gio/widget/dnd_test.go delete mode 100644 gio/widget/doc.go delete mode 100644 gio/widget/editor.go delete mode 100644 gio/widget/editor_test.go delete mode 100644 gio/widget/enum.go delete mode 100644 gio/widget/example_test.go delete mode 100644 gio/widget/fit.go delete mode 100644 gio/widget/fit_test.go delete mode 100644 gio/widget/float.go delete mode 100644 gio/widget/icon.go delete mode 100644 gio/widget/icon_test.go delete mode 100644 gio/widget/image.go delete mode 100644 gio/widget/image_test.go delete mode 100644 gio/widget/index.go delete mode 100644 gio/widget/index_test.go delete mode 100644 gio/widget/label.go delete mode 100644 gio/widget/label_test.go delete mode 100644 gio/widget/list.go delete mode 100644 gio/widget/material/button.go delete mode 100644 gio/widget/material/checkable.go delete mode 100644 gio/widget/material/checkbox.go delete mode 100644 gio/widget/material/decorations.go delete mode 100644 gio/widget/material/doc.go delete mode 100644 gio/widget/material/editor.go delete mode 100644 gio/widget/material/label.go delete mode 100644 gio/widget/material/list.go delete mode 100644 gio/widget/material/list_test.go delete mode 100644 gio/widget/material/loader.go delete mode 100644 gio/widget/material/progressbar.go delete mode 100644 gio/widget/material/progresscircle.go delete mode 100644 gio/widget/material/radiobutton.go delete mode 100644 gio/widget/material/slider.go delete mode 100644 gio/widget/material/switch.go delete mode 100644 gio/widget/material/theme.go delete mode 100644 gio/widget/selectable.go delete mode 100644 gio/widget/selectable_test.go delete mode 100644 gio/widget/text.go delete mode 100644 gio/widget/text_bench_test.go delete mode 100644 gio/widget/widget_test.go diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..921af96 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,27 @@ +{ + "permissions": { + "allow": [ + "WebSearch", + "WebFetch(domain:arxiv.org)", + "WebFetch(domain:hal.science)", + "WebFetch(domain:github.com)", + "WebFetch(domain:www.researchgate.net)", + "WebFetch(domain:www.semanticscholar.org)", + "Bash(ls:*)", + "Bash(grep:*)", + "Bash(xargs:*)", + "Bash(find:*)", + "Bash(rm:*)", + "Bash(go mod tidy:*)", + "Bash(go get:*)", + "Bash(go build:*)", + "Bash(mkdir:*)", + "Bash(GOOS=darwin GOARCH=amd64 go build:*)", + "Bash(GOOS=windows GOARCH=amd64 go build:*)", + "Bash(GOOS=js GOARCH=wasm go build:*)", + "Bash(GOOS=android GOARCH=arm64 go build:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/app.go b/app.go index 37930d6..7c61c2b 100644 --- a/app.go +++ b/app.go @@ -6,8 +6,8 @@ import ( "go.uber.org/atomic" "golang.org/x/exp/shiny/materialdesign/icons" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/unit" ) // App defines an application with a header, sidebar/menu, right side button bar, changeable body page widget and @@ -57,7 +57,7 @@ type App struct { type WidgetMap map[string]l.Widget -func (w *Window) App(size *atomic.Int32, activePage *atomic.String, Break1 float32, ) *App { +func (w *Window) App(size *atomic.Int32, activePage *atomic.String, Break1 float32) *App { // mc := w.Clickable() a := &App{ Window: w, @@ -211,9 +211,8 @@ func (a *App) MainFrame(gtx l.Context) l.Dimensions { }, }, { - Size: a.Break1, - Widget: - a.renderSideBar(), + Size: a.Break1, + Widget: a.renderSideBar(), }, }, ).Fn, diff --git a/bool.go b/bool.go index 2827925..a9ab8c1 100644 --- a/bool.go +++ b/bool.go @@ -1,7 +1,7 @@ package gel import ( - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" ) type BoolHook func(b bool) diff --git a/border.go b/border.go index 8974e17..207673a 100644 --- a/border.go +++ b/border.go @@ -4,10 +4,10 @@ import ( "image" "image/color" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" ) // Border lays out a widget and draws a border inside it. diff --git a/button.go b/button.go index b56bbd2..b2c2d0e 100644 --- a/button.go +++ b/button.go @@ -6,15 +6,15 @@ import ( "math" "strings" - "github.com/p9c/p9/pkg/gel/gio/font" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" + "gioui.org/font" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/text" + "gioui.org/unit" - "github.com/p9c/p9/pkg/gel/f32color" + "git.mleku.dev/mleku/prevara/f32color" ) // Button is a material text label icon with options to change all features @@ -190,7 +190,7 @@ func drawInk(c l.Context, p press) { // Too old. return } - + alphat = half1 + half2 } // Compute the expand position in [0;1]. diff --git a/buttonlayout.go b/buttonlayout.go index a8ce82a..2042097 100644 --- a/buttonlayout.go +++ b/buttonlayout.go @@ -4,11 +4,11 @@ import ( "image" "image/color" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/unit" - "github.com/p9c/p9/pkg/gel/f32color" + "git.mleku.dev/mleku/prevara/f32color" ) type ButtonLayout struct { diff --git a/card.go b/card.go index 3851adc..6bdee4b 100644 --- a/card.go +++ b/card.go @@ -1,6 +1,6 @@ package gel -import l "github.com/p9c/p9/pkg/gel/gio/layout" +import l "gioui.org/layout" func (w *Window) Card(background string, embed l.Widget, ) func(gtx l.Context) l.Dimensions { diff --git a/checkable.go b/checkable.go index 5098f9a..14e936d 100644 --- a/checkable.go +++ b/checkable.go @@ -3,12 +3,12 @@ package gel import ( "image" - "github.com/p9c/p9/pkg/gel/gio/font" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" + "gioui.org/font" + l "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/text" + "gioui.org/unit" "golang.org/x/exp/shiny/materialdesign/icons" ) diff --git a/checkbox.go b/checkbox.go index 9411de0..a3edb85 100644 --- a/checkbox.go +++ b/checkbox.go @@ -1,8 +1,8 @@ package gel import ( - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/unit" ) // CheckBox creates a checkbox with a text label diff --git a/clickable.go b/clickable.go index 5a46413..980275b 100644 --- a/clickable.go +++ b/clickable.go @@ -4,10 +4,10 @@ import ( "image" "time" - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/key" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" + "gioui.org/gesture" + "gioui.org/io/key" + l "gioui.org/layout" + "gioui.org/op/clip" ) // clickEvents holds callback functions for click interaction events. diff --git a/clipboard/log.go b/clipboard/log.go index 4368bfe..a88adee 100644 --- a/clipboard/log.go +++ b/clipboard/log.go @@ -1,9 +1,9 @@ package clipboard import ( - "github.com/p9c/p9/pkg/log" + "git.mleku.dev/mleku/prevara/pkg/log" - "github.com/p9c/p9/version" + "git.mleku.dev/mleku/prevara/version" ) var subsystem = log.AddLoggerSubsystem(version.PathBase) diff --git a/cmd/clipboard/logmain.go b/cmd/clipboard/logmain.go index 58579ab..cc438ec 100644 --- a/cmd/clipboard/logmain.go +++ b/cmd/clipboard/logmain.go @@ -1,9 +1,9 @@ package main import ( - "github.com/p9c/p9/pkg/log" + "git.mleku.dev/mleku/prevara/pkg/log" - "github.com/p9c/p9/version" + "git.mleku.dev/mleku/prevara/version" ) var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/cmd/clipboard/main.go b/cmd/clipboard/main.go index 3c31d11..567d126 100644 --- a/cmd/clipboard/main.go +++ b/cmd/clipboard/main.go @@ -1,12 +1,12 @@ package main import ( - "github.com/p9c/p9/pkg/qu" + "git.mleku.dev/mleku/prevara/pkg/qu" - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" - "github.com/p9c/p9/pkg/gel" - "github.com/p9c/p9/pkg/gel/clipboard" + "git.mleku.dev/mleku/prevara" + "git.mleku.dev/mleku/prevara/clipboard" ) type State struct { diff --git a/cmd/hello/logmain.go b/cmd/hello/logmain.go index 58579ab..cc438ec 100644 --- a/cmd/hello/logmain.go +++ b/cmd/hello/logmain.go @@ -1,9 +1,9 @@ package main import ( - "github.com/p9c/p9/pkg/log" + "git.mleku.dev/mleku/prevara/pkg/log" - "github.com/p9c/p9/version" + "git.mleku.dev/mleku/prevara/version" ) var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/cmd/hello/main.go b/cmd/hello/main.go index 79f9d38..65a7d29 100644 --- a/cmd/hello/main.go +++ b/cmd/hello/main.go @@ -1,10 +1,10 @@ package main import ( - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/qu" + l "gioui.org/layout" + "git.mleku.dev/mleku/prevara/pkg/qu" - "github.com/p9c/p9/pkg/gel" + "git.mleku.dev/mleku/prevara" ) type State struct { diff --git a/cmd/iconchooser/logmain.go b/cmd/iconchooser/logmain.go index 58579ab..cc438ec 100644 --- a/cmd/iconchooser/logmain.go +++ b/cmd/iconchooser/logmain.go @@ -1,9 +1,9 @@ package main import ( - "github.com/p9c/p9/pkg/log" + "git.mleku.dev/mleku/prevara/pkg/log" - "github.com/p9c/p9/version" + "git.mleku.dev/mleku/prevara/version" ) var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/cmd/iconchooser/main.go b/cmd/iconchooser/main.go index 2024c0a..6599270 100644 --- a/cmd/iconchooser/main.go +++ b/cmd/iconchooser/main.go @@ -3,14 +3,14 @@ package main import ( "sort" - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" + "git.mleku.dev/mleku/prevara/pkg/interrupt" + "git.mleku.dev/mleku/prevara/pkg/qu" "github.com/atotto/clipboard" - "github.com/p9c/p9/pkg/interrupt" - "github.com/p9c/p9/pkg/qu" - "github.com/p9c/p9/pkg/gel/icons" + "git.mleku.dev/mleku/prevara/icons" - "github.com/p9c/p9/pkg/gel" + "git.mleku.dev/mleku/prevara" ) type State struct { diff --git a/column.go b/column.go index 3b12adc..3c1bd47 100644 --- a/column.go +++ b/column.go @@ -1,7 +1,7 @@ package gel import ( - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" ) type ColumnRow struct { @@ -52,7 +52,7 @@ func (c *Column) List(gtx l.Context) (max int, out []l.Widget) { _ = i out = append(out, func(gtx l.Context) l.Dimensions { return c.Flex(). // AlignEnd().SpaceStart(). - Rigid( + Rigid( c.Fill("red", l.Center, float32(c.TextSize), 0, EmptySpace(max-dims[i].Size.X, dims[i].Size.Y)).Fn, ). Rigid( @@ -94,7 +94,7 @@ func (c *Column) List(gtx l.Context) (max int, out []l.Widget) { // return out[index](gtx) // } return max, out - + // // render the widgets onto a second context to get their dimensions // gtx1 = CopyContextDimensionsWithMaxAxis(gtx, gtx.Constraints.Max, l.Vertical) // dim := GetDimension(gtx1, c.Theme.SliceToWidget(out, l.Vertical)) diff --git a/dialog/dialog.go_ b/dialog/dialog.go_ index 9ee8206..4baa216 100644 --- a/dialog/dialog.go_ +++ b/dialog/dialog.go_ @@ -4,11 +4,11 @@ import ( "image" "image/color" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + "git.mleku.dev/mleku/prevara/gio/io/pointer" + l "git.mleku.dev/mleku/prevara/gio/layout" + "git.mleku.dev/mleku/prevara/gio/op" + "git.mleku.dev/mleku/prevara/gio/op/paint" + "git.mleku.dev/mleku/prevara/gio/unit" "github.com/p9c/p9/pkg/gui" ) diff --git a/dialog/example/main.go_ b/dialog/example/main.go_ index 62d4655..d74eeee 100644 --- a/dialog/example/main.go_ +++ b/dialog/example/main.go_ @@ -4,12 +4,12 @@ import ( "log" "os" - "github.com/p9c/p9/pkg/gel/gio/app" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + "git.mleku.dev/mleku/prevara/gio/app" + "git.mleku.dev/mleku/prevara/gio/io/system" + "git.mleku.dev/mleku/prevara/gio/layout" + "git.mleku.dev/mleku/prevara/gio/op" + "git.mleku.dev/mleku/prevara/gio/op/paint" + "git.mleku.dev/mleku/prevara/gio/unit" "github.com/p9c/p9/pkg/gui" "github.com/p9c/p9/pkg/gui/dialog" diff --git a/dimensionlist.go b/dimensionlist.go index f916156..be4d90c 100644 --- a/dimensionlist.go +++ b/dimensionlist.go @@ -1,8 +1,8 @@ package gel import ( - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" + l "gioui.org/layout" + "gioui.org/op" ) type DimensionList []l.Dimensions diff --git a/direction.go b/direction.go index 558ac1e..eace87a 100644 --- a/direction.go +++ b/direction.go @@ -1,6 +1,6 @@ package gel -import l "github.com/p9c/p9/pkg/gel/gio/layout" +import l "gioui.org/layout" type Direction struct { l.Direction diff --git a/editor.go b/editor.go index 931d2c4..89d6db5 100644 --- a/editor.go +++ b/editor.go @@ -7,15 +7,15 @@ import ( "image/color" "time" - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/io/key" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" + "gioui.org/f32" + "gioui.org/font" + "gioui.org/io/key" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/paint" + "gioui.org/text" + "gioui.org/unit" + "gioui.org/widget" "golang.org/x/image/math/fixed" ) diff --git a/editor_test.go_ b/editor_test.go_ index 3ffdbfc..58cccc0 100644 --- a/editor_test.go_ +++ b/editor_test.go_ @@ -12,14 +12,14 @@ import ( "testing/quick" "unicode" - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/font/gofont" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" + "git.mleku.dev/mleku/prevara/gio/f32" + "git.mleku.dev/mleku/prevara/gio/font/gofont" + "git.mleku.dev/mleku/prevara/gio/io/event" + "git.mleku.dev/mleku/prevara/gio/io/key" + "git.mleku.dev/mleku/prevara/gio/layout" + "git.mleku.dev/mleku/prevara/gio/op" + "git.mleku.dev/mleku/prevara/gio/text" + "git.mleku.dev/mleku/prevara/gio/unit" ) func TestEditor(t *testing.T) { diff --git a/enum.go b/enum.go index a960d79..2527443 100644 --- a/enum.go +++ b/enum.go @@ -3,9 +3,9 @@ package gel import ( "image" - "github.com/p9c/p9/pkg/gel/gio/gesture" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" + "gioui.org/gesture" + l "gioui.org/layout" + "gioui.org/op/clip" ) type Enum struct { diff --git a/fill.go b/fill.go index 5f9b11d..0fef9fe 100644 --- a/fill.go +++ b/fill.go @@ -4,9 +4,9 @@ import ( "image" "image/color" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" + l "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/op/paint" ) // Filler fills the background of a widget with a specified color and corner diff --git a/flex.go b/flex.go index d036070..c0a1306 100644 --- a/flex.go +++ b/flex.go @@ -1,6 +1,6 @@ package gel -import l "github.com/p9c/p9/pkg/gel/gio/layout" +import l "gioui.org/layout" type Flex struct { flex l.Flex diff --git a/float.go b/float.go index a4244e8..1947248 100644 --- a/float.go +++ b/float.go @@ -5,10 +5,10 @@ package gel import ( "image" - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" + "gioui.org/gesture" + "gioui.org/io/pointer" + l "gioui.org/layout" + "gioui.org/op/clip" ) // Float is for selecting a value in a range. diff --git a/fonts/bariolbold/data.go b/fonts/bariolbold/data.go index 5521f75..98a6c58 100644 --- a/fonts/bariolbold/data.go +++ b/fonts/bariolbold/data.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT package bariolbold diff --git a/fonts/bariolbolditalic/data.go b/fonts/bariolbolditalic/data.go index ef47c5e..c6ca85b 100644 --- a/fonts/bariolbolditalic/data.go +++ b/fonts/bariolbolditalic/data.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT package bariolbolditalic diff --git a/fonts/bariollight/data.go b/fonts/bariollight/data.go index eb68861..aea992d 100644 --- a/fonts/bariollight/data.go +++ b/fonts/bariollight/data.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT package bariollight diff --git a/fonts/bariollightitalic/data.go b/fonts/bariollightitalic/data.go index c942a1d..4350db8 100644 --- a/fonts/bariollightitalic/data.go +++ b/fonts/bariollightitalic/data.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT package bariollightitalic diff --git a/fonts/bariolregular/data.go b/fonts/bariolregular/data.go index 85f62ee..19bd99d 100644 --- a/fonts/bariolregular/data.go +++ b/fonts/bariolregular/data.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT package bariolregular diff --git a/fonts/bariolregularitalic/data.go b/fonts/bariolregularitalic/data.go index 9927cd3..a9c19f9 100644 --- a/fonts/bariolregularitalic/data.go +++ b/fonts/bariolregularitalic/data.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT package bariolregularitalic diff --git a/fonts/bariolthin/data.go b/fonts/bariolthin/data.go index b3902dc..930fe98 100644 --- a/fonts/bariolthin/data.go +++ b/fonts/bariolthin/data.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT package bariolthin diff --git a/fonts/bariolthinitalic/data.go b/fonts/bariolthinitalic/data.go index 550b44d..0b312f0 100644 --- a/fonts/bariolthinitalic/data.go +++ b/fonts/bariolthinitalic/data.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT package bariolthinitalic diff --git a/fonts/logmain.go b/fonts/logmain.go index 58579ab..cc438ec 100644 --- a/fonts/logmain.go +++ b/fonts/logmain.go @@ -1,9 +1,9 @@ package main import ( - "github.com/p9c/p9/pkg/log" + "git.mleku.dev/mleku/prevara/pkg/log" - "github.com/p9c/p9/version" + "git.mleku.dev/mleku/prevara/version" ) var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/fonts/main.go b/fonts/main.go index 885733d..30418ba 100644 --- a/fonts/main.go +++ b/fonts/main.go @@ -38,7 +38,7 @@ func main() { if e = ttfs.Close(); E.Chk(e) { } }() - + infos, e := ttfs.Readdir(-1) if e != nil { F.Ln(e) @@ -62,14 +62,14 @@ func do(ttfName string) { if e != nil { F.Ln(e) } - + // desc := "a proportional-width, sans-serif" // if strings.Contains(ttfName, "Mono") { // desc = "a fixed-width, slab-serif" // } - + b := new(bytes.Buffer) - _, _ = fmt.Fprintf(b, "// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT\n\n") + _, _ = fmt.Fprintf(b, "// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT\n\n") _, _ = fmt.Fprintf(b, "package %s\n\n", pkgName) _, _ = fmt.Fprintf(b, "// TTF is the data for the %q TrueType font.\n", fontName) _, _ = fmt.Fprintf(b, "var TTF = []byte{") @@ -80,7 +80,7 @@ func do(ttfName string) { _, _ = fmt.Fprintf(b, "%#02x,", x) } _, _ = fmt.Fprintf(b, "\n}\n") - + dst, e := format.Source(b.Bytes()) if e != nil { F.Ln(e) diff --git a/fonts/p9fonts/fonts.go b/fonts/p9fonts/fonts.go index 8e9ad11..3e0375d 100644 --- a/fonts/p9fonts/fonts.go +++ b/fonts/p9fonts/fonts.go @@ -4,20 +4,20 @@ import ( "fmt" "sync" - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/font/opentype" + "gioui.org/font" + "gioui.org/font/opentype" "golang.org/x/image/font/gofont/gomono" "golang.org/x/image/font/gofont/gomonobold" "golang.org/x/image/font/gofont/gomonobolditalic" "golang.org/x/image/font/gofont/gomonoitalic" - "github.com/p9c/p9/pkg/gel/fonts/bariolbold" - "github.com/p9c/p9/pkg/gel/fonts/bariolbolditalic" - "github.com/p9c/p9/pkg/gel/fonts/bariollight" - "github.com/p9c/p9/pkg/gel/fonts/bariollightitalic" - "github.com/p9c/p9/pkg/gel/fonts/bariolregular" - "github.com/p9c/p9/pkg/gel/fonts/bariolregularitalic" - "github.com/p9c/p9/pkg/gel/fonts/plan9" + "git.mleku.dev/mleku/prevara/fonts/bariolbold" + "git.mleku.dev/mleku/prevara/fonts/bariolbolditalic" + "git.mleku.dev/mleku/prevara/fonts/bariollight" + "git.mleku.dev/mleku/prevara/fonts/bariollightitalic" + "git.mleku.dev/mleku/prevara/fonts/bariolregular" + "git.mleku.dev/mleku/prevara/fonts/bariolregularitalic" + "git.mleku.dev/mleku/prevara/fonts/plan9" ) var ( diff --git a/fonts/plan9/data.go b/fonts/plan9/data.go index d4edd7c..af0e045 100644 --- a/fonts/plan9/data.go +++ b/fonts/plan9/data.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/fonts; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/fonts; DO NOT EDIT package plan9 diff --git a/gio/.builds/apple.yml b/gio/.builds/apple.yml deleted file mode 100644 index 5ffcd24..0000000 --- a/gio/.builds/apple.yml +++ /dev/null @@ -1,74 +0,0 @@ -# SPDX-License-Identifier: Unlicense OR MIT -image: debian/testing -packages: - - clang - - cmake - - curl - - autoconf - - libxml2-dev - - libssl-dev - - libz-dev - - llvm-dev # cctools - - uuid-dev # cctools - - ninja-build # cctools - - systemtap-sdt-dev # cctools - - libbsd-dev # cctools - - linux-libc-dev # cctools - - libplist-utils # for gogio -sources: - - https://git.sr.ht/~eliasnaur/applesdks - - https://git.sr.ht/~eliasnaur/gio - - https://git.sr.ht/~eliasnaur/giouiorg - - https://github.com/tpoechtrager/cctools-port - - https://github.com/tpoechtrager/apple-libtapi - - https://github.com/tpoechtrager/apple-libdispatch - - https://github.com/mackyle/xar -environment: - APPLE_TOOLCHAIN_ROOT: /home/build/appletools - PATH: /home/build/sdk/go/bin:/home/build/go/bin:/usr/bin -tasks: - - install_go: | - mkdir -p /home/build/sdk - curl -s https://dl.google.com/go/go1.24.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - - - prepare_toolchain: | - mkdir -p $APPLE_TOOLCHAIN_ROOT - cd $APPLE_TOOLCHAIN_ROOT - tar xJf /home/build/applesdks/applesdks.tar.xz - mkdir bin tools - cd bin - ln -s ../toolchain/bin/x86_64-apple-darwin19-ld ld - ln -s ../toolchain/bin/x86_64-apple-darwin19-ar ar - ln -s /home/build/cctools-port/cctools/misc/lipo lipo - ln -s ../tools/appletoolchain xcrun - ln -s /usr/bin/plistutil plutil - cd ../tools - ln -s appletoolchain clang-ios - ln -s appletoolchain clang-macos - - install_appletoolchain: | - cd giouiorg - go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain - - build_libdispatch: | - cd apple-libdispatch - cmake -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_INSTALL_PREFIX=$APPLE_TOOLCHAIN_ROOT/libdispatch . - ninja - ninja install - - build_xar: | - cd xar/xar - ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr - make - sudo make install - - build_libtapi: | - cd apple-libtapi - INSTALLPREFIX=$APPLE_TOOLCHAIN_ROOT/libtapi ./build.sh - ./install.sh - - build_cctools: | - cd cctools-port/cctools - ./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 - make install - - test_macos: | - cd gio - export PATH=/home/build/appletools/bin:$PATH - CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./... - - test_ios: | - cd gio - 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 ./... diff --git a/gio/.builds/freebsd.yml b/gio/.builds/freebsd.yml deleted file mode 100644 index 42d6eb6..0000000 --- a/gio/.builds/freebsd.yml +++ /dev/null @@ -1,22 +0,0 @@ -# SPDX-License-Identifier: Unlicense OR MIT -image: freebsd/latest -packages: - - libX11 - - libxkbcommon - - libXcursor - - libXfixes - - vulkan-headers - - wayland - - mesa-libs - - xorg-vfbserver -sources: - - https://git.sr.ht/~eliasnaur/gio -environment: - PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin -tasks: - - install_go: | - mkdir -p /home/build/sdk - curl https://dl.google.com/go/go1.24.2.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf - - - test_gio: | - cd gio - go test ./... diff --git a/gio/.builds/linux.yml b/gio/.builds/linux.yml deleted file mode 100644 index ed14307..0000000 --- a/gio/.builds/linux.yml +++ /dev/null @@ -1,90 +0,0 @@ -# SPDX-License-Identifier: Unlicense OR MIT -image: debian/testing -packages: - - curl - - pkg-config - - gcc-multilib - - libwayland-dev - - libx11-dev - - libx11-xcb-dev - - libxkbcommon-dev - - libxkbcommon-x11-dev - - libgles2-mesa-dev - - libegl1-mesa-dev - - libffi-dev - - libvulkan-dev - - libxcursor-dev - - libxrandr-dev - - libxinerama-dev - - libxi-dev - - libxxf86vm-dev - - mesa-vulkan-drivers - - wine - - xvfb - - xdotool - - scrot - - sway - - grim - - unzip -sources: - - https://git.sr.ht/~eliasnaur/gio -environment: - PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig/:/usr/lib/i386-linux-gnu/pkgconfig/ - PATH: /home/build/sdk/go/bin:/usr/bin:/home/build/go/bin:/home/build/android/tools/bin - ANDROID_SDK_ROOT: /home/build/android - android_sdk_tools_zip: sdk-tools-linux-3859397.zip - android_ndk_zip: android-ndk-r20-linux-x86_64.zip - github_mirror: git@github.com:gioui/gio -secrets: - - 75d8a1eb-5fc5-4074-8a36-db6015d6ed5a -tasks: - - install_go: | - mkdir -p /home/build/sdk - curl -s https://dl.google.com/go/go1.24.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - - - check_gofmt: | - cd gio - test -z "$(gofmt -s -l .)" - - check_sign_off: | - set +x -e - cd gio - for hash in $(git log -n 20 --format="%H"); do - message=$(git log -1 --format=%B $hash) - if [[ ! "$message" =~ "Signed-off-by: " ]]; then - echo "Missing 'Signed-off-by' in commit $hash" - exit 1 - fi - done - - mirror: | - # mirror to github - ssh-keyscan github.com > "$HOME"/.ssh/known_hosts && cd gio && git push --mirror "$github_mirror" || echo "failed mirroring" - - add_32bit_arch: | - sudo dpkg --add-architecture i386 - sudo apt-get update - 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" - - test_gio: | - cd gio - go test -race ./... - CGO_ENABLED=1 GOARCH=386 go test ./... - GOOS=windows go test -exec=wine ./... - GOOS=js GOARCH=wasm go build -o /dev/null ./... - - install_jdk8: | - curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb" - sudo apt-get -qq install -y -f ./jdk.deb - - install_android: | - mkdir android - cd android - curl -so sdk-tools.zip https://dl.google.com/android/repository/$android_sdk_tools_zip - unzip -q sdk-tools.zip - rm sdk-tools.zip - curl -so ndk.zip https://dl.google.com/android/repository/$android_ndk_zip - unzip -q ndk.zip - rm ndk.zip - mv android-ndk-* ndk-bundle - # sdkmanager needs lots of file descriptors - ulimit -n 10000 - yes|sdkmanager --licenses - sdkmanager "platforms;android-31" "build-tools;32.0.0" - - test_android: | - cd gio - 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 ./... - 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 ./... diff --git a/gio/.builds/openbsd.yml b/gio/.builds/openbsd.yml deleted file mode 100644 index 775db3e..0000000 --- a/gio/.builds/openbsd.yml +++ /dev/null @@ -1,18 +0,0 @@ -# SPDX-License-Identifier: Unlicense OR MIT -image: openbsd/latest -packages: - - libxkbcommon - - go -sources: - - https://git.sr.ht/~eliasnaur/gio -environment: - PATH: /home/build/sdk/go/bin:/bin:/usr/local/bin:/usr/bin -tasks: - - install_go: | - mkdir -p /home/build/sdk - curl https://dl.google.com/go/go1.24.2.src.tar.gz | tar -C /home/build/sdk -xzf - - cd /home/build/sdk/go/src - ./make.bash - - test_gio: | - cd gio - go test ./... diff --git a/gio/LICENSE b/gio/LICENSE deleted file mode 100644 index 81f4733..0000000 --- a/gio/LICENSE +++ /dev/null @@ -1,63 +0,0 @@ -This project is provided under the terms of the UNLICENSE or -the MIT license denoted by the following SPDX identifier: - -SPDX-License-Identifier: Unlicense OR MIT - -You may use the project under the terms of either license. - -Both licenses are reproduced below. - ----- -The MIT License (MIT) - -Copyright (c) 2019 The Gio authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. ---- - - - ---- -The UNLICENSE - -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to ---- diff --git a/gio/LOCAL_MODIFICATIONS.md b/gio/LOCAL_MODIFICATIONS.md deleted file mode 100644 index 9cbbc05..0000000 --- a/gio/LOCAL_MODIFICATIONS.md +++ /dev/null @@ -1,50 +0,0 @@ -# Local Modifications to Vendored GIO - -## Import Path Changes - -The original GIO library uses import paths starting with `gioui.org/`. -This vendored copy has been modified to use `github.com/p9c/p9/pkg/gel/gio/`. - -### Examples of modified imports: - -| Original GIO Path | Vendored Path | -|-------------------|---------------| -| `gioui.org/app` | `github.com/p9c/p9/pkg/gel/gio/app` | -| `gioui.org/layout` | `github.com/p9c/p9/pkg/gel/gio/layout` | -| `gioui.org/op` | `github.com/p9c/p9/pkg/gel/gio/op` | -| `gioui.org/unit` | `github.com/p9c/p9/pkg/gel/gio/unit` | -| `gioui.org/widget` | `github.com/p9c/p9/pkg/gel/gio/widget` | -| `gioui.org/shader` | `github.com/p9c/p9/pkg/gel/gio/shader` | -| etc. | etc. | - -## Version Information - -**Updated: November 2024** - -This vendored copy is now from the latest GIO (as of November 2024): -- Go 1.23+ required -- New API patterns (unit.Dp/unit.Sp typed units, input.Source, etc.) -- Package structure includes io/input (renamed from io/router) -- Shader module included from gioui.org/shader v1.0.8 - -## External Dependencies - -The following external dependencies are still required (not vendored): -- `eliasnaur.com/font` - Font collection -- `github.com/go-text/typesetting` - Text layout and shaping - -## Removed Components - -The following were removed from the vendor: -- `shader/piet/` - Compute shader programs (require gioui.org/cpu) -- `shader/cmd/` - Shader compilation tools - -## When Updating - -After replacing with a newer GIO, all imports need to be rewritten: -- `gioui.org/...` → `github.com/p9c/p9/pkg/gel/gio/...` - -This can be done with: -```bash -find pkg/gel/gio -name "*.go" -type f -exec sed -i 's|"gioui.org/|"github.com/p9c/p9/pkg/gel/gio/|g' {} \; -``` diff --git a/gio/README.md b/gio/README.md deleted file mode 100644 index 3707eb6..0000000 --- a/gio/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Gio - https://gioui.org - -Immediate mode GUI programs in Go for Android, iOS, macOS, Linux, -FreeBSD, OpenBSD, Windows, and WebAssembly (experimental). - -# Installation, examples, documentation - -Go to [gioui.org](https://gioui.org). - -[![builds.sr.ht status](https://builds.sr.ht/~eliasnaur/gio.svg)](https://builds.sr.ht/~eliasnaur/gio) - -## Issues - -File bugs and TODOs through the [issue tracker](https://todo.sr.ht/~eliasnaur/gio) or send an email -to [~eliasnaur/gio@todo.sr.ht](mailto:~eliasnaur/gio@todo.sr.ht). For general discussion, use the -mailing list: [~eliasnaur/gio@lists.sr.ht](mailto:~eliasnaur/gio@lists.sr.ht). - -## Contributing - -Post discussion to the [mailing list](https://lists.sr.ht/~eliasnaur/gio) and patches to -[gio-patches](https://lists.sr.ht/~eliasnaur/gio-patches). No Sourcehut -account is required and you can post without being subscribed. - -See the [contribution guide](https://gioui.org/doc/contribute) for more details. - -An [official GitHub mirror](https://github.com/gioui/gio) is available. - -## Tags - -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. - -Tags follow semantic versioning. In particular, as the major version is zero: - -- breaking API or behavior changes will increment the *minor* version component. -- non-breaking changes will increment the *patch* version component. diff --git a/gio/app/app.go b/gio/app/app.go deleted file mode 100644 index 08dc3a3..0000000 --- a/gio/app/app.go +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "image" - "os" - "path/filepath" - "strings" - "time" - - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// extraArgs contains extra arguments to append to -// os.Args. The arguments are separated with |. -// Useful for running programs on mobiles where the -// command line is not available. -// Set with the go linker flag -X. -var extraArgs string - -// ID is the app id exposed to the platform. -// -// On Android ID is the package property of AndroidManifest.xml, -// on iOS ID is the CFBundleIdentifier of the app Info.plist, -// on Wayland it is the toplevel app_id, -// on X11 it is the X11 XClassHint. -// -// ID is set by the [gioui.org/cmd/gogio] tool or manually with the -X linker flag. For example, -// -// go build -ldflags="-X 'gioui.org/app.ID=org.gioui.example.Kitchen'" . -// -// Note that ID is treated as a constant, and that changing it at runtime -// is not supported. The default value of ID is filepath.Base(os.Args[0]). -var ID = "" - -// A FrameEvent requests a new frame in the form of a list of -// operations that describes the window content. -type FrameEvent struct { - // Now is the current animation. Use Now instead of time.Now to - // synchronize animation and to avoid the time.Now call overhead. - Now time.Time - // Metric converts device independent dp and sp to device pixels. - Metric unit.Metric - // Size is the dimensions of the window. - Size image.Point - // Insets represent the space occupied by system decorations and controls. - Insets Insets - // Frame completes the FrameEvent by drawing the graphical operations - // from ops into the window. - Frame func(frame *op.Ops) - // Source is the interface between the window and widgets. - Source input.Source -} - -// ViewEvent provides handles to the underlying window objects for the -// current display protocol. -type ViewEvent interface { - implementsViewEvent() - ImplementsEvent() - // Valid will return true when the ViewEvent does contains valid handles. - // If a window receives an invalid ViewEvent, it should deinitialize any - // state referring to handles from a previous ViewEvent. - Valid() bool -} - -// Insets is the space taken up by -// system decoration such as translucent -// system bars and software keyboards. -type Insets struct { - // Values are in pixels. - Top, Bottom, Left, Right unit.Dp -} - -// NewContext is shorthand for -// -// layout.Context{ -// Ops: ops, -// Now: e.Now, -// Source: e.Source, -// Metric: e.Metric, -// Constraints: layout.Exact(e.Size), -// } -// -// NewContext calls ops.Reset and adjusts ops for e.Insets. -func NewContext(ops *op.Ops, e FrameEvent) layout.Context { - ops.Reset() - - size := e.Size - - if e.Insets != (Insets{}) { - left := e.Metric.Dp(e.Insets.Left) - top := e.Metric.Dp(e.Insets.Top) - op.Offset(image.Point{ - X: left, - Y: top, - }).Add(ops) - - size.X -= left + e.Metric.Dp(e.Insets.Right) - size.Y -= top + e.Metric.Dp(e.Insets.Bottom) - } - - return layout.Context{ - Ops: ops, - Now: e.Now, - Source: e.Source, - Metric: e.Metric, - Constraints: layout.Exact(size), - } -} - -// DataDir returns a path to use for application-specific -// configuration data. -// On desktop systems, DataDir use os.UserConfigDir. -// On iOS NSDocumentDirectory is queried. -// For Android Context.getFilesDir is used. -// -// BUG: DataDir blocks on Android until init functions -// have completed. -func DataDir() (string, error) { - return dataDir() -} - -// Main must be called last from the program main function. -// On most platforms Main blocks forever, for Android and -// iOS it returns immediately to give control of the main -// thread back to the system. -// -// Calling Main is necessary because some operating systems -// require control of the main thread of the program for -// running windows. -func Main() { - osMain() -} - -func (FrameEvent) ImplementsEvent() {} - -func init() { - if extraArgs != "" { - args := strings.Split(extraArgs, "|") - os.Args = append(os.Args, args...) - } - if ID == "" { - ID = filepath.Base(os.Args[0]) - } -} diff --git a/gio/app/d3d11_windows.go b/gio/app/d3d11_windows.go deleted file mode 100644 index 3c8a881..0000000 --- a/gio/app/d3d11_windows.go +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "fmt" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/d3d11" -) - -type d3d11Context struct { - win *window - dev *d3d11.Device - ctx *d3d11.DeviceContext - - swchain *d3d11.IDXGISwapChain - renderTarget *d3d11.RenderTargetView - width, height int -} - -const debugDirectX = false - -func init() { - drivers = append(drivers, gpuAPI{ - priority: 1, - initializer: func(w *window) (context, error) { - hwnd, _, _ := w.HWND() - var flags uint32 - if debugDirectX { - flags |= d3d11.CREATE_DEVICE_DEBUG - } - dev, ctx, _, err := d3d11.CreateDevice( - d3d11.DRIVER_TYPE_HARDWARE, - flags, - ) - if err != nil { - return nil, fmt.Errorf("NewContext: %v", err) - } - swchain, err := d3d11.CreateSwapChain(dev, hwnd) - if err != nil { - d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release) - d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release) - return nil, err - } - return &d3d11Context{win: w, dev: dev, ctx: ctx, swchain: swchain}, nil - }, - }) -} - -func (c *d3d11Context) API() gpu.API { - return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)} -} - -func (c *d3d11Context) RenderTarget() (gpu.RenderTarget, error) { - return gpu.Direct3D11RenderTarget{ - RenderTarget: unsafe.Pointer(c.renderTarget), - }, nil -} - -func (c *d3d11Context) Present() error { - return wrapErr(c.swchain.Present(1, 0)) -} - -func wrapErr(err error) error { - if err, ok := err.(d3d11.ErrorCode); ok { - switch err.Code { - case d3d11.DXGI_STATUS_OCCLUDED: - // Ignore - return nil - case d3d11.DXGI_ERROR_DEVICE_RESET, d3d11.DXGI_ERROR_DEVICE_REMOVED, d3d11.D3DDDIERR_DEVICEREMOVED: - return gpu.ErrDeviceLost - } - } - return err -} - -func (c *d3d11Context) Refresh() error { - var width, height int - _, width, height = c.win.HWND() - if c.renderTarget != nil && width == c.width && height == c.height { - return nil - } - c.releaseFBO() - if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, 0); err != nil { - return wrapErr(err) - } - c.width = width - c.height = height - - backBuffer, err := c.swchain.GetBuffer(0, &d3d11.IID_Texture2D) - if err != nil { - return err - } - texture := (*d3d11.Resource)(unsafe.Pointer(backBuffer)) - renderTarget, err := c.dev.CreateRenderTargetView(texture) - d3d11.IUnknownRelease(unsafe.Pointer(backBuffer), backBuffer.Vtbl.Release) - if err != nil { - return err - } - c.renderTarget = renderTarget - return nil -} - -func (c *d3d11Context) Lock() error { - c.ctx.OMSetRenderTargets(c.renderTarget, nil) - return nil -} - -func (c *d3d11Context) Unlock() {} - -func (c *d3d11Context) Release() { - c.releaseFBO() - if c.swchain != nil { - d3d11.IUnknownRelease(unsafe.Pointer(c.swchain), c.swchain.Vtbl.Release) - } - if c.ctx != nil { - d3d11.IUnknownRelease(unsafe.Pointer(c.ctx), c.ctx.Vtbl.Release) - } - if c.dev != nil { - d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release) - } - *c = d3d11Context{} - if debugDirectX { - d3d11.ReportLiveObjects() - } -} - -func (c *d3d11Context) releaseFBO() { - if c.renderTarget != nil { - d3d11.IUnknownRelease(unsafe.Pointer(c.renderTarget), c.renderTarget.Vtbl.Release) - c.renderTarget = nil - } -} diff --git a/gio/app/datadir.go b/gio/app/datadir.go deleted file mode 100644 index 500a59a..0000000 --- a/gio/app/datadir.go +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build !android -// +build !android - -package app - -import "os" - -func dataDir() (string, error) { - return os.UserConfigDir() -} diff --git a/gio/app/doc.go b/gio/app/doc.go deleted file mode 100644 index a5c50e7..0000000 --- a/gio/app/doc.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package app provides a platform-independent interface to operating system -functionality for running graphical user interfaces. - -See https://gioui.org for instructions to set up and run Gio programs. - -# Windows - -A Window is run by calling its Event method in a loop. The first time a -method on Window is called, a new GUI window is created and shown. On mobile -platforms or when Gio is embedded in another project, Window merely connects -with a previously created GUI window. - -The most important event is [FrameEvent] that prompts an update of the window -contents. - -For example: - - w := new(app.Window) - for { - e := w.Event() - if e, ok := e.(app.FrameEvent); ok { - ops.Reset() - // Add operations to ops. - ... - // Completely replace the window contents and state. - e.Frame(ops) - } - } - -A program must keep receiving events from the event channel until -[DestroyEvent] is received. - -# Main - -The Main function must be called from a program's main function, to hand over -control of the main thread to operating systems that need it. - -Because Main is also blocking on some platforms, the event loop of a Window must run in a goroutine. - -For example, to display a blank but otherwise functional window: - - package main - - import "github.com/p9c/p9/pkg/gel/gio/app" - - func main() { - go func() { - w := app.NewWindow() - for { - w.Event() - } - }() - app.Main() - } - -# Permissions - -The packages under gioui.org/app/permission should be imported -by a Gio program or by one of its dependencies to indicate that specific -operating-system permissions are required. Please see documentation for -package gioui.org/app/permission for more information. -*/ -package app diff --git a/gio/app/egl_android.go b/gio/app/egl_android.go deleted file mode 100644 index 2badbe3..0000000 --- a/gio/app/egl_android.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build !noopengl - -package app - -/* -#include -#include -*/ -import "C" - -import ( - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/internal/egl" -) - -type androidContext struct { - win *window - eglSurf egl.NativeWindowType - *egl.Context -} - -func init() { - newAndroidGLESContext = func(w *window) (context, error) { - ctx, err := egl.NewContext(nil) - if err != nil { - return nil, err - } - return &androidContext{win: w, Context: ctx}, nil - } -} - -func (c *androidContext) Release() { - if c.Context != nil { - c.Context.Release() - c.Context = nil - } -} - -func (c *androidContext) Refresh() error { - c.Context.ReleaseSurface() - if err := c.win.setVisual(c.Context.VisualID()); err != nil { - return err - } - win, _, _ := c.win.nativeWindow() - c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win)) - return nil -} - -func (c *androidContext) Lock() error { - // The Android emulator creates a broken surface if it is not - // created on the same thread as the context is made current. - if c.eglSurf != nil { - if err := c.Context.CreateSurface(c.eglSurf); err != nil { - return err - } - c.eglSurf = nil - } - return c.Context.MakeCurrent() -} - -func (c *androidContext) Unlock() { - c.Context.ReleaseCurrent() -} diff --git a/gio/app/egl_wayland.go b/gio/app/egl_wayland.go deleted file mode 100644 index 1c9c94f..0000000 --- a/gio/app/egl_wayland.go +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build ((linux && !android) || freebsd) && !nowayland && !noopengl -// +build linux,!android freebsd -// +build !nowayland -// +build !noopengl - -package app - -import ( - "errors" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/internal/egl" -) - -/* -#cgo linux pkg-config: egl wayland-egl -#cgo freebsd openbsd LDFLAGS: -lwayland-egl -#cgo CFLAGS: -DEGL_NO_X11 - -#include -#include -#include -*/ -import "C" - -type wlContext struct { - win *window - *egl.Context - eglWin *C.struct_wl_egl_window -} - -func init() { - newWaylandEGLContext = func(w *window) (context, error) { - disp := egl.NativeDisplayType(unsafe.Pointer(w.display())) - ctx, err := egl.NewContext(disp) - if err != nil { - return nil, err - } - - surf, width, height := w.surface() - if surf == nil { - return nil, errors.New("wayland: no surface") - } - eglWin := C.wl_egl_window_create(surf, C.int(width), C.int(height)) - if eglWin == nil { - return nil, errors.New("wayland: wl_egl_window_create failed") - } - eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin))) - if err := ctx.CreateSurface(eglSurf); err != nil { - return nil, err - } - // We're in charge of the frame callbacks, don't let eglSwapBuffers - // wait for callbacks that may never arrive. - ctx.EnableVSync(false) - - return &wlContext{Context: ctx, win: w, eglWin: eglWin}, nil - } -} - -func (c *wlContext) Release() { - if c.Context != nil { - c.Context.Release() - c.Context = nil - } - if c.eglWin != nil { - C.wl_egl_window_destroy(c.eglWin) - c.eglWin = nil - } -} - -func (c *wlContext) Refresh() error { - surf, width, height := c.win.surface() - if surf == nil { - return errors.New("wayland: no surface") - } - C.wl_egl_window_resize(c.eglWin, C.int(width), C.int(height), 0, 0) - return nil -} - -func (c *wlContext) Lock() error { - return c.Context.MakeCurrent() -} - -func (c *wlContext) Unlock() { - c.Context.ReleaseCurrent() -} diff --git a/gio/app/egl_windows.go b/gio/app/egl_windows.go deleted file mode 100644 index fb12199..0000000 --- a/gio/app/egl_windows.go +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build !noopengl - -package app - -import ( - "github.com/p9c/p9/pkg/gel/gio/internal/egl" -) - -type glContext struct { - win *window - *egl.Context -} - -func init() { - drivers = append(drivers, gpuAPI{ - priority: 2, - initializer: func(w *window) (context, error) { - disp := egl.NativeDisplayType(w.HDC()) - ctx, err := egl.NewContext(disp) - if err != nil { - return nil, err - } - win, _, _ := w.HWND() - eglSurf := egl.NativeWindowType(win) - if err := ctx.CreateSurface(eglSurf); err != nil { - ctx.Release() - return nil, err - } - if err := ctx.MakeCurrent(); err != nil { - ctx.Release() - return nil, err - } - defer ctx.ReleaseCurrent() - ctx.EnableVSync(true) - return &glContext{win: w, Context: ctx}, nil - }, - }) -} - -func (c *glContext) Release() { - if c.Context != nil { - c.Context.Release() - c.Context = nil - } -} - -func (c *glContext) Refresh() error { - return nil -} - -func (c *glContext) Lock() error { - return c.Context.MakeCurrent() -} - -func (c *glContext) Unlock() { - c.Context.ReleaseCurrent() -} diff --git a/gio/app/egl_x11.go b/gio/app/egl_x11.go deleted file mode 100644 index 369bd17..0000000 --- a/gio/app/egl_x11.go +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build ((linux && !android) || freebsd || openbsd) && !nox11 && !noopengl -// +build linux,!android freebsd openbsd -// +build !nox11 -// +build !noopengl - -package app - -import ( - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/internal/egl" -) - -type x11Context struct { - win *x11Window - *egl.Context -} - -func init() { - newX11EGLContext = func(w *x11Window) (context, error) { - disp := egl.NativeDisplayType(unsafe.Pointer(w.display())) - ctx, err := egl.NewContext(disp) - if err != nil { - return nil, err - } - win, _, _ := w.window() - eglSurf := egl.NativeWindowType(uintptr(win)) - if err := ctx.CreateSurface(eglSurf); err != nil { - ctx.Release() - return nil, err - } - if err := ctx.MakeCurrent(); err != nil { - ctx.Release() - return nil, err - } - defer ctx.ReleaseCurrent() - ctx.EnableVSync(true) - return &x11Context{win: w, Context: ctx}, nil - } -} - -func (c *x11Context) Release() { - if c.Context != nil { - c.Context.Release() - c.Context = nil - } -} - -func (c *x11Context) Refresh() error { - return nil -} - -func (c *x11Context) Lock() error { - return c.Context.MakeCurrent() -} - -func (c *x11Context) Unlock() { - c.Context.ReleaseCurrent() -} diff --git a/gio/app/framework_ios.h b/gio/app/framework_ios.h deleted file mode 100644 index 18e5a02..0000000 --- a/gio/app/framework_ios.h +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -#include - -@interface GioViewController : UIViewController -@end diff --git a/gio/app/gl_ios.go b/gio/app/gl_ios.go deleted file mode 100644 index c762205..0000000 --- a/gio/app/gl_ios.go +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build darwin && ios && nometal -// +build darwin,ios,nometal - -package app - -/* -@import UIKit; - -#include -#include -#include - -__attribute__ ((visibility ("hidden"))) int gio_renderbufferStorage(CFTypeRef ctx, CFTypeRef layer, GLenum buffer); -__attribute__ ((visibility ("hidden"))) int gio_presentRenderbuffer(CFTypeRef ctx, GLenum buffer); -__attribute__ ((visibility ("hidden"))) int gio_makeCurrent(CFTypeRef ctx); -__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createContext(void); -__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLLayer(void); - -static CFTypeRef getViewLayer(CFTypeRef viewRef) { - @autoreleasepool { - UIView *view = (__bridge UIView *)viewRef; - return CFBridgingRetain(view.layer); - } -} - -*/ -import "C" - -import ( - "errors" - "fmt" - "runtime" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/gl" -) - -type context struct { - owner *window - c *gl.Functions - ctx C.CFTypeRef - layer C.CFTypeRef - init bool - frameBuffer gl.Framebuffer - colorBuffer gl.Renderbuffer -} - -func newContext(w *window) (*context, error) { - ctx := C.gio_createContext() - if ctx == 0 { - return nil, fmt.Errorf("failed to create EAGLContext") - } - api := contextAPI() - f, err := gl.NewFunctions(api.Context, api.ES) - if err != nil { - return nil, err - } - c := &context{ - ctx: ctx, - owner: w, - layer: C.getViewLayer(w.contextView()), - c: f, - } - return c, nil -} - -func contextAPI() gpu.OpenGL { - return gpu.OpenGL{} -} - -func (c *context) RenderTarget() gpu.RenderTarget { - return gpu.OpenGLRenderTarget(c.frameBuffer) -} - -func (c *context) API() gpu.API { - return contextAPI() -} - -func (c *context) Release() { - if c.ctx == 0 { - return - } - C.gio_renderbufferStorage(c.ctx, 0, C.GLenum(gl.RENDERBUFFER)) - c.c.DeleteFramebuffer(c.frameBuffer) - c.c.DeleteRenderbuffer(c.colorBuffer) - C.gio_makeCurrent(0) - C.CFRelease(c.ctx) - c.ctx = 0 -} - -func (c *context) Present() error { - if c.layer == 0 { - panic("context is not active") - } - c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer) - if C.gio_presentRenderbuffer(c.ctx, C.GLenum(gl.RENDERBUFFER)) == 0 { - return errors.New("presentRenderBuffer failed") - } - return nil -} - -func (c *context) Lock() error { - // OpenGL contexts are implicit and thread-local. Lock the OS thread. - runtime.LockOSThread() - - if C.gio_makeCurrent(c.ctx) == 0 { - return errors.New("[EAGLContext setCurrentContext] failed") - } - return nil -} - -func (c *context) Unlock() { - C.gio_makeCurrent(0) -} - -func (c *context) Refresh() error { - if C.gio_makeCurrent(c.ctx) == 0 { - return errors.New("[EAGLContext setCurrentContext] failed") - } - if !c.init { - c.init = true - c.frameBuffer = c.c.CreateFramebuffer() - c.colorBuffer = c.c.CreateRenderbuffer() - } - if !c.owner.visible { - // Make sure any in-flight GL commands are complete. - c.c.Finish() - return nil - } - currentRB := gl.Renderbuffer{uint(c.c.GetInteger(gl.RENDERBUFFER_BINDING))} - c.c.BindRenderbuffer(gl.RENDERBUFFER, c.colorBuffer) - if C.gio_renderbufferStorage(c.ctx, c.layer, C.GLenum(gl.RENDERBUFFER)) == 0 { - return errors.New("renderbufferStorage failed") - } - c.c.BindRenderbuffer(gl.RENDERBUFFER, currentRB) - c.c.BindFramebuffer(gl.FRAMEBUFFER, c.frameBuffer) - c.c.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, c.colorBuffer) - if st := c.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { - return fmt.Errorf("framebuffer incomplete, status: %#x\n", st) - } - return nil -} - -func (w *window) NewContext() (Context, error) { - return newContext(w) -} diff --git a/gio/app/gl_js.go b/gio/app/gl_js.go deleted file mode 100644 index 3cc2ac3..0000000 --- a/gio/app/gl_js.go +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "errors" - "syscall/js" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/gl" -) - -type glContext struct { - ctx js.Value - cnv js.Value - w *window -} - -func newContext(w *window) (*glContext, error) { - args := map[string]interface{}{ - // Enable low latency rendering. - // See https://developers.google.com/web/updates/2019/05/desynchronized. - "desynchronized": true, - "preserveDrawingBuffer": true, - } - ctx := w.cnv.Call("getContext", "webgl2", args) - if ctx.IsNull() { - ctx = w.cnv.Call("getContext", "webgl", args) - } - if ctx.IsNull() { - return nil, errors.New("app: webgl is not supported") - } - c := &glContext{ - ctx: ctx, - cnv: w.cnv, - w: w, - } - return c, nil -} - -func (c *glContext) RenderTarget() (gpu.RenderTarget, error) { - if c.w.contextStatus != contextStatusOkay { - return nil, gpu.ErrDeviceLost - } - return gpu.OpenGLRenderTarget{}, nil -} - -func (c *glContext) API() gpu.API { - return gpu.OpenGL{Context: gl.Context(c.ctx)} -} - -func (c *glContext) Release() { -} - -func (c *glContext) Present() error { - return nil -} - -func (c *glContext) Lock() error { - return nil -} - -func (c *glContext) Unlock() {} - -func (c *glContext) Refresh() error { - switch c.w.contextStatus { - case contextStatusLost: - return errOutOfDate - case contextStatusRestored: - c.w.contextStatus = contextStatusOkay - return gpu.ErrDeviceLost - default: - return nil - } -} - -func (w *window) NewContext() (context, error) { - return newContext(w) -} diff --git a/gio/app/gl_macos.go b/gio/app/gl_macos.go deleted file mode 100644 index c1600c2..0000000 --- a/gio/app/gl_macos.go +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build darwin && !ios && nometal -// +build darwin,!ios,nometal - -package app - -import ( - "errors" - "runtime" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/gl" -) - -/* -#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -xobjective-c -fobjc-arc -#cgo LDFLAGS: -framework OpenGL - -#include -#include -#include -#include - -__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createGLContext(void); -__attribute__ ((visibility ("hidden"))) void gio_setContextView(CFTypeRef ctx, CFTypeRef view); -__attribute__ ((visibility ("hidden"))) void gio_makeCurrentContext(CFTypeRef ctx); -__attribute__ ((visibility ("hidden"))) void gio_updateContext(CFTypeRef ctx); -__attribute__ ((visibility ("hidden"))) void gio_flushContextBuffer(CFTypeRef ctx); -__attribute__ ((visibility ("hidden"))) void gio_clearCurrentContext(void); -__attribute__ ((visibility ("hidden"))) void gio_lockContext(CFTypeRef ctxRef); -__attribute__ ((visibility ("hidden"))) void gio_unlockContext(CFTypeRef ctxRef); - -typedef void (*PFN_glFlush)(void); - -static void glFlush(PFN_glFlush f) { - f(); -} -*/ -import "C" - -type glContext struct { - c *gl.Functions - ctx C.CFTypeRef - view C.CFTypeRef - - glFlush C.PFN_glFlush -} - -func newContext(w *window) (*glContext, error) { - clib := C.CString("/System/Library/Frameworks/OpenGL.framework/OpenGL") - defer C.free(unsafe.Pointer(clib)) - lib, err := C.dlopen(clib, C.RTLD_NOW|C.RTLD_LOCAL) - if err != nil { - return nil, err - } - csym := C.CString("glFlush") - defer C.free(unsafe.Pointer(csym)) - glFlush := C.PFN_glFlush(C.dlsym(lib, csym)) - if glFlush == nil { - return nil, errors.New("gl: missing symbol glFlush in the OpenGL framework") - } - view := w.contextView() - ctx := C.gio_createGLContext() - if ctx == 0 { - return nil, errors.New("gl: failed to create NSOpenGLContext") - } - C.gio_setContextView(ctx, view) - c := &glContext{ - ctx: ctx, - view: view, - glFlush: glFlush, - } - return c, nil -} - -func (c *glContext) RenderTarget() (gpu.RenderTarget, error) { - return gpu.OpenGLRenderTarget{}, nil -} - -func (c *glContext) API() gpu.API { - return gpu.OpenGL{} -} - -func (c *glContext) Release() { - if c.ctx != 0 { - C.gio_clearCurrentContext() - C.CFRelease(c.ctx) - c.ctx = 0 - } -} - -func (c *glContext) Present() error { - // Assume the caller already locked the context. - C.glFlush(c.glFlush) - return nil -} - -func (c *glContext) Lock() error { - // OpenGL contexts are implicit and thread-local. Lock the OS thread. - runtime.LockOSThread() - - C.gio_lockContext(c.ctx) - C.gio_makeCurrentContext(c.ctx) - return nil -} - -func (c *glContext) Unlock() { - C.gio_clearCurrentContext() - C.gio_unlockContext(c.ctx) -} - -func (c *glContext) Refresh() error { - c.Lock() - defer c.Unlock() - C.gio_updateContext(c.ctx) - return nil -} - -func (w *window) NewContext() (context, error) { - return newContext(w) -} diff --git a/gio/app/ime.go b/gio/app/ime.go deleted file mode 100644 index 02383b6..0000000 --- a/gio/app/ime.go +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "unicode" - "unicode/utf16" - "unicode/utf8" - - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/key" -) - -type editorState struct { - input.EditorState - compose key.Range -} - -func (e *editorState) Replace(r key.Range, text string) { - if r.Start > r.End { - r.Start, r.End = r.End, r.Start - } - runes := []rune(text) - newEnd := r.Start + len(runes) - adjust := func(pos int) int { - switch { - case newEnd < pos && pos <= r.End: - return newEnd - case r.End < pos: - diff := newEnd - r.End - return pos + diff - } - return pos - } - e.Selection.Start = adjust(e.Selection.Start) - e.Selection.End = adjust(e.Selection.End) - if e.compose.Start != -1 { - e.compose.Start = adjust(e.compose.Start) - e.compose.End = adjust(e.compose.End) - } - s := e.Snippet - if r.End < s.Start || r.Start > s.End { - // Discard snippet if it doesn't overlap with replacement. - s = key.Snippet{ - Range: key.Range{ - Start: r.Start, - End: r.Start, - }, - } - } - var newSnippet []rune - snippet := []rune(s.Text) - // Append first part of existing snippet. - if end := r.Start - s.Start; end > 0 { - newSnippet = append(newSnippet, snippet[:end]...) - } - // Append replacement. - newSnippet = append(newSnippet, runes...) - // Append last part of existing snippet. - if start := r.End; start < s.End { - newSnippet = append(newSnippet, snippet[start-s.Start:]...) - } - // Adjust snippet range to include replacement. - if r.Start < s.Start { - s.Start = r.Start - } - s.End = s.Start + len(newSnippet) - s.Text = string(newSnippet) - e.Snippet = s -} - -// UTF16Index converts the given index in runes into an index in utf16 characters. -func (e *editorState) UTF16Index(runes int) int { - if runes == -1 { - return -1 - } - if runes < e.Snippet.Start { - // Assume runes before sippet are one UTF-16 character each. - return runes - } - chars := e.Snippet.Start - runes -= e.Snippet.Start - for _, r := range e.Snippet.Text { - if runes == 0 { - break - } - runes-- - chars++ - if r1, _ := utf16.EncodeRune(r); r1 != unicode.ReplacementChar { - chars++ - } - } - // Assume runes after snippets are one UTF-16 character each. - return chars + runes -} - -// RunesIndex converts the given index in utf16 characters to an index in runes. -func (e *editorState) RunesIndex(chars int) int { - if chars == -1 { - return -1 - } - if chars < e.Snippet.Start { - // Assume runes before offset are one UTF-16 character each. - return chars - } - runes := e.Snippet.Start - chars -= e.Snippet.Start - for _, r := range e.Snippet.Text { - if chars == 0 { - break - } - chars-- - runes++ - if r1, _ := utf16.EncodeRune(r); r1 != unicode.ReplacementChar { - chars-- - } - } - // Assume runes after snippets are one UTF-16 character each. - return runes + chars -} - -// areSnippetsConsistent reports whether the content of the old snippet is -// consistent with the content of the new. -func areSnippetsConsistent(old, new key.Snippet) bool { - // Compute the overlapping range. - r := old.Range - r.Start = max(r.Start, new.Start) - r.End = max(r.End, r.Start) - r.End = min(r.End, new.End) - return snippetSubstring(old, r) == snippetSubstring(new, r) -} - -func snippetSubstring(s key.Snippet, r key.Range) string { - for r.Start > s.Start && r.Start < s.End { - _, n := utf8.DecodeRuneInString(s.Text) - s.Text = s.Text[n:] - s.Start++ - } - for r.End < s.End && r.End > s.Start { - _, n := utf8.DecodeLastRuneInString(s.Text) - s.Text = s.Text[:len(s.Text)-n] - s.End-- - } - return s.Text -} diff --git a/gio/app/ime_test.go b/gio/app/ime_test.go deleted file mode 100644 index 3f56866..0000000 --- a/gio/app/ime_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build go1.18 -// +build go1.18 - -package app - -import ( - "github.com/p9c/p9/pkg/gel/gio/f32" - "testing" - "unicode/utf8" - - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/font/gofont" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -func FuzzIME(f *testing.F) { - runes := []rune("Hello, 世界! 🤬 علي،الحسنب北查爾斯頓工廠的安全漏洞已") - f.Add([]byte("20\x0010")) - f.Add([]byte("80000")) - f.Add([]byte("2008\"80\r00")) - f.Add([]byte("20007900002\x02000")) - f.Add([]byte("20007800002\x02000")) - f.Add([]byte("200A02000990\x19002\x17\x0200")) - f.Fuzz(func(t *testing.T, cmds []byte) { - cache := text.NewShaper(text.WithCollection(gofont.Collection())) - e := new(widget.Editor) - - var r input.Router - gtx := layout.Context{Ops: new(op.Ops), Source: r.Source()} - gtx.Execute(key.FocusCmd{Tag: e}) - // Layout once to register focus. - e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - - var state editorState - state.Selection.Transform = f32.AffineId() - const ( - cmdReplace = iota - cmdSelect - cmdSnip - maxCmd - ) - const cmdLen = 5 - for len(cmds) >= cmdLen { - n := e.Len() - rng := key.Range{ - Start: int(cmds[1]) % (n + 1), - End: int(cmds[2]) % (n + 1), - } - switch cmds[0] % cmdLen { - case cmdReplace: - rstart := int(cmds[3]) % len(runes) - rend := int(cmds[4]) % len(runes) - if rstart > rend { - rstart, rend = rend, rstart - } - replacement := string(runes[rstart:rend]) - state.Replace(rng, replacement) - r.Queue(key.EditEvent{Range: rng, Text: replacement}) - r.Queue(key.SnippetEvent(state.Snippet.Range)) - case cmdSelect: - r.Queue(key.SelectionEvent(rng)) - runes := []rune(e.Text()) - if rng.Start < 0 { - rng.Start = 0 - } - if rng.End < 0 { - rng.End = 0 - } - if rng.Start > len(runes) { - rng.Start = len(runes) - } - if rng.End > len(runes) { - rng.End = len(runes) - } - state.Selection.Range = rng - case cmdSnip: - r.Queue(key.SnippetEvent(rng)) - runes := []rune(e.Text()) - if rng.Start > rng.End { - rng.Start, rng.End = rng.End, rng.Start - } - if rng.Start < 0 { - rng.Start = 0 - } - if rng.End < 0 { - rng.End = 0 - } - if rng.Start > len(runes) { - rng.Start = len(runes) - } - if rng.End > len(runes) { - rng.End = len(runes) - } - state.Snippet = key.Snippet{ - Range: rng, - Text: string(runes[rng.Start:rng.End]), - } - } - cmds = cmds[cmdLen:] - e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - newState := r.EditorState() - // We don't track caret position. - state.Selection.Caret = newState.Selection.Caret - // Expanded snippets are ok. - their, our := newState.Snippet, state.EditorState.Snippet - beforeLen := 0 - for before := our.Start - their.Start; before > 0; before-- { - _, n := utf8.DecodeRuneInString(their.Text[beforeLen:]) - beforeLen += n - } - afterLen := 0 - for after := their.End - our.End; after > 0; after-- { - _, n := utf8.DecodeLastRuneInString(their.Text[:len(their.Text)-afterLen]) - afterLen += n - } - if beforeLen > 0 { - our.Text = their.Text[:beforeLen] + our.Text - our.Start = their.Start - } - if afterLen > 0 { - our.Text = our.Text + their.Text[len(their.Text)-afterLen:] - our.End = their.End - } - state.EditorState.Snippet = our - if newState != state.EditorState { - t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState) - } - } - }) -} - -func TestEditorIndices(t *testing.T) { - var s editorState - s.Selection.Transform = f32.AffineId() - const str = "Hello, 😀" - s.Snippet = key.Snippet{ - Text: str, - Range: key.Range{ - Start: 10, - End: utf8.RuneCountInString(str), - }, - } - utf16Indices := [...]struct { - Runes, UTF16 int - }{ - {0, 0}, {10, 10}, {17, 17}, {18, 19}, {30, 31}, - } - for _, p := range utf16Indices { - if want, got := p.UTF16, s.UTF16Index(p.Runes); want != got { - t.Errorf("UTF16Index(%d) = %d, wanted %d", p.Runes, got, want) - } - if want, got := p.Runes, s.RunesIndex(p.UTF16); want != got { - t.Errorf("RunesIndex(%d) = %d, wanted %d", p.UTF16, got, want) - } - } -} diff --git a/gio/app/internal/windows/windows.go b/gio/app/internal/windows/windows.go deleted file mode 100644 index b23392f..0000000 --- a/gio/app/internal/windows/windows.go +++ /dev/null @@ -1,960 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build windows -// +build windows - -package windows - -import ( - "fmt" - "runtime" - "time" - "unicode/utf16" - "unsafe" - - syscall "golang.org/x/sys/windows" -) - -type CompositionForm struct { - dwStyle uint32 - ptCurrentPos Point - rcArea Rect -} - -type CandidateForm struct { - dwIndex uint32 - dwStyle uint32 - ptCurrentPos Point - rcArea Rect -} - -type Rect struct { - Left, Top, Right, Bottom int32 -} - -type WndClassEx struct { - CbSize uint32 - Style uint32 - LpfnWndProc uintptr - CnClsExtra int32 - CbWndExtra int32 - HInstance syscall.Handle - HIcon syscall.Handle - HCursor syscall.Handle - HbrBackground syscall.Handle - LpszMenuName *uint16 - LpszClassName *uint16 - HIconSm syscall.Handle -} - -type Margins struct { - CxLeftWidth int32 - CxRightWidth int32 - CyTopHeight int32 - CyBottomHeight int32 -} - -type Msg struct { - Hwnd syscall.Handle - Message uint32 - WParam uintptr - LParam uintptr - Time uint32 - Pt Point - LPrivate uint32 -} - -type Point struct { - X, Y int32 -} - -type MinMaxInfo struct { - PtReserved Point - PtMaxSize Point - PtMaxPosition Point - PtMinTrackSize Point - PtMaxTrackSize Point -} - -type NCCalcSizeParams struct { - Rgrc [3]Rect - LpPos *WindowPos -} - -type WindowPos struct { - HWND syscall.Handle - HWNDInsertAfter syscall.Handle - x int32 - y int32 - cx int32 - cy int32 - flags uint32 -} - -type WindowPlacement struct { - length uint32 - flags uint32 - showCmd uint32 - ptMinPosition Point - ptMaxPosition Point - rcNormalPosition Rect - rcDevice Rect -} - -type MonitorInfo struct { - cbSize uint32 - Monitor Rect - WorkArea Rect - Flags uint32 -} - -type POINTER_INPUT_TYPE int32 - -const ( - PT_POINTER POINTER_INPUT_TYPE = 1 - PT_TOUCH POINTER_INPUT_TYPE = 2 - PT_PEN POINTER_INPUT_TYPE = 3 - PT_MOUSE POINTER_INPUT_TYPE = 4 - PT_TOUCHPAD POINTER_INPUT_TYPE = 5 -) - -type POINTER_INFO_POINTER_FLAGS int32 - -const ( - POINTER_FLAG_NEW POINTER_INFO_POINTER_FLAGS = 0x00000001 - POINTER_FLAG_INRANGE POINTER_INFO_POINTER_FLAGS = 0x00000002 - POINTER_FLAG_INCONTACT POINTER_INFO_POINTER_FLAGS = 0x00000004 - POINTER_FLAG_FIRSTBUTTON POINTER_INFO_POINTER_FLAGS = 0x00000010 - POINTER_FLAG_SECONDBUTTON POINTER_INFO_POINTER_FLAGS = 0x00000020 - POINTER_FLAG_THIRDBUTTON POINTER_INFO_POINTER_FLAGS = 0x00000040 - POINTER_FLAG_FOURTHBUTTON POINTER_INFO_POINTER_FLAGS = 0x00000080 - POINTER_FLAG_FIFTHBUTTON POINTER_INFO_POINTER_FLAGS = 0x00000100 - POINTER_FLAG_PRIMARY POINTER_INFO_POINTER_FLAGS = 0x00002000 - POINTER_FLAG_CONFIDENCE POINTER_INFO_POINTER_FLAGS = 0x00004000 - POINTER_FLAG_CANCELED POINTER_INFO_POINTER_FLAGS = 0x00008000 - POINTER_FLAG_DOWN POINTER_INFO_POINTER_FLAGS = 0x00010000 - POINTER_FLAG_UPDATE POINTER_INFO_POINTER_FLAGS = 0x00020000 - POINTER_FLAG_UP POINTER_INFO_POINTER_FLAGS = 0x00040000 - POINTER_FLAG_WHEEL POINTER_INFO_POINTER_FLAGS = 0x00080000 - POINTER_FLAG_HWHEEL POINTER_INFO_POINTER_FLAGS = 0x00100000 - POINTER_FLAG_CAPTURECHANGED POINTER_INFO_POINTER_FLAGS = 0x00200000 - POINTER_FLAG_HASTRANSFORM POINTER_INFO_POINTER_FLAGS = 0x00400000 -) - -type POINTER_BUTTON_CHANGE_TYPE int32 - -const ( - POINTER_CHANGE_NONE POINTER_BUTTON_CHANGE_TYPE = 0 - POINTER_CHANGE_FIRSTBUTTON_DOWN POINTER_BUTTON_CHANGE_TYPE = 1 - POINTER_CHANGE_FIRSTBUTTON_UP POINTER_BUTTON_CHANGE_TYPE = 2 - POINTER_CHANGE_SECONDBUTTON_DOWN POINTER_BUTTON_CHANGE_TYPE = 3 - POINTER_CHANGE_SECONDBUTTON_UP POINTER_BUTTON_CHANGE_TYPE = 4 - POINTER_CHANGE_THIRDBUTTON_DOWN POINTER_BUTTON_CHANGE_TYPE = 5 - POINTER_CHANGE_THIRDBUTTON_UP POINTER_BUTTON_CHANGE_TYPE = 6 - POINTER_CHANGE_FOURTHBUTTON_DOWN POINTER_BUTTON_CHANGE_TYPE = 7 - POINTER_CHANGE_FOURTHBUTTON_UP POINTER_BUTTON_CHANGE_TYPE = 8 - POINTER_CHANGE_FIFTHBUTTON_DOWN POINTER_BUTTON_CHANGE_TYPE = 9 - POINTER_CHANGE_FIFTHBUTTON_UP POINTER_BUTTON_CHANGE_TYPE = 10 -) - -type PointerInfo struct { - PointerType POINTER_INPUT_TYPE - PointerId uint32 - FrameId uint32 - PointerFlags POINTER_INFO_POINTER_FLAGS - SourceDevice syscall.Handle - HwndTarget syscall.Handle - PtPixelLocation Point - PtHimetricLocation Point - PtPixelLocationRaw Point - PtHimetricLocationRaw Point - DwTime uint32 - HistoryCount uint32 - InputData int32 - DwKeyStates uint32 - PerformanceCount uint64 - ButtonChangeType POINTER_BUTTON_CHANGE_TYPE -} - -const ( - TRUE = 1 - - CPS_CANCEL = 0x0004 - - CS_HREDRAW = 0x0002 - CS_INSERTCHAR = 0x2000 - CS_NOMOVECARET = 0x4000 - CS_VREDRAW = 0x0001 - CS_OWNDC = 0x0020 - - CW_USEDEFAULT = -2147483648 - - GWL_STYLE = ^(uintptr(16) - 1) // -16 - - GCS_COMPSTR = 0x0008 - GCS_COMPREADSTR = 0x0001 - GCS_CURSORPOS = 0x0080 - GCS_DELTASTART = 0x0100 - GCS_RESULTREADSTR = 0x0200 - GCS_RESULTSTR = 0x0800 - - CFS_POINT = 0x0002 - CFS_CANDIDATEPOS = 0x0040 - - HWND_TOPMOST = ^(uint32(1) - 1) // -1 - - HTCAPTION = 2 - HTCLIENT = 1 - HTLEFT = 10 - HTRIGHT = 11 - HTTOP = 12 - HTTOPLEFT = 13 - HTTOPRIGHT = 14 - HTBOTTOM = 15 - HTBOTTOMLEFT = 16 - HTBOTTOMRIGHT = 17 - - IDC_APPSTARTING = 32650 // Standard arrow and small hourglass - IDC_ARROW = 32512 // Standard arrow - IDC_CROSS = 32515 // Crosshair - IDC_HAND = 32649 // Hand - IDC_HELP = 32651 // Arrow and question mark - IDC_IBEAM = 32513 // I-beam - IDC_NO = 32648 // Slashed circle - IDC_SIZEALL = 32646 // Four-pointed arrow pointing north, south, east, and west - IDC_SIZENESW = 32643 // Double-pointed arrow pointing northeast and southwest - IDC_SIZENS = 32645 // Double-pointed arrow pointing north and south - IDC_SIZENWSE = 32642 // Double-pointed arrow pointing northwest and southeast - IDC_SIZEWE = 32644 // Double-pointed arrow pointing west and east - IDC_UPARROW = 32516 // Vertical arrow - IDC_WAIT = 32514 // Hour - - INFINITE = 0xFFFFFFFF - - LOGPIXELSX = 88 - - MDT_EFFECTIVE_DPI = 0 - - MONITOR_DEFAULTTOPRIMARY = 1 - - NI_COMPOSITIONSTR = 0x0015 - - SIZE_MAXIMIZED = 2 - SIZE_MINIMIZED = 1 - SIZE_RESTORED = 0 - - SCS_SETSTR = GCS_COMPREADSTR | GCS_COMPSTR - - SM_CXSIZEFRAME = 32 - SM_CYSIZEFRAME = 33 - - SW_SHOWDEFAULT = 10 - SW_SHOWMINIMIZED = 2 - SW_SHOWMAXIMIZED = 3 - SW_SHOWNORMAL = 1 - SW_SHOW = 5 - - SWP_FRAMECHANGED = 0x0020 - SWP_NOMOVE = 0x0002 - SWP_NOOWNERZORDER = 0x0200 - SWP_NOSIZE = 0x0001 - SWP_NOZORDER = 0x0004 - SWP_SHOWWINDOW = 0x0040 - - USER_TIMER_MINIMUM = 0x0000000A - - VK_CONTROL = 0x11 - VK_LWIN = 0x5B - VK_MENU = 0x12 - VK_RWIN = 0x5C - VK_SHIFT = 0x10 - - VK_BACK = 0x08 - VK_DELETE = 0x2e - VK_DOWN = 0x28 - VK_END = 0x23 - VK_ESCAPE = 0x1b - VK_HOME = 0x24 - VK_LEFT = 0x25 - VK_NEXT = 0x22 - VK_PRIOR = 0x21 - VK_RIGHT = 0x27 - VK_RETURN = 0x0d - VK_SPACE = 0x20 - VK_TAB = 0x09 - VK_UP = 0x26 - - VK_F1 = 0x70 - VK_F2 = 0x71 - VK_F3 = 0x72 - VK_F4 = 0x73 - VK_F5 = 0x74 - VK_F6 = 0x75 - VK_F7 = 0x76 - VK_F8 = 0x77 - VK_F9 = 0x78 - VK_F10 = 0x79 - VK_F11 = 0x7A - VK_F12 = 0x7B - - VK_OEM_1 = 0xba - VK_OEM_PLUS = 0xbb - VK_OEM_COMMA = 0xbc - VK_OEM_MINUS = 0xbd - VK_OEM_PERIOD = 0xbe - VK_OEM_2 = 0xbf - VK_OEM_3 = 0xc0 - VK_OEM_4 = 0xdb - VK_OEM_5 = 0xdc - VK_OEM_6 = 0xdd - VK_OEM_7 = 0xde - VK_OEM_102 = 0xe2 - - UNICODE_NOCHAR = 65535 - - WM_CANCELMODE = 0x001F - WM_CHAR = 0x0102 - WM_CLOSE = 0x0010 - WM_CREATE = 0x0001 - WM_DPICHANGED = 0x02E0 - WM_DESTROY = 0x0002 - WM_ERASEBKGND = 0x0014 - WM_GETMINMAXINFO = 0x0024 - WM_IME_COMPOSITION = 0x010F - WM_IME_ENDCOMPOSITION = 0x010E - WM_IME_STARTCOMPOSITION = 0x010D - WM_KEYDOWN = 0x0100 - WM_KEYUP = 0x0101 - WM_KILLFOCUS = 0x0008 - WM_LBUTTONDOWN = 0x0201 - WM_LBUTTONUP = 0x0202 - WM_MBUTTONDOWN = 0x0207 - WM_MBUTTONUP = 0x0208 - WM_MOUSEMOVE = 0x0200 - WM_MOUSEWHEEL = 0x020A - WM_MOUSEHWHEEL = 0x020E - WM_NCACTIVATE = 0x0086 - WM_NCHITTEST = 0x0084 - WM_NCCALCSIZE = 0x0083 - WM_PAINT = 0x000F - WM_POINTERCAPTURECHANGED = 0x024C - WM_POINTERDOWN = 0x0246 - WM_POINTERUP = 0x0247 - WM_POINTERUPDATE = 0x0245 - WM_POINTERWHEEL = 0x024E - WM_POINTERHWHEEL = 0x024F - WM_QUIT = 0x0012 - WM_RBUTTONDOWN = 0x0204 - WM_RBUTTONUP = 0x0205 - WM_SETCURSOR = 0x0020 - WM_SETFOCUS = 0x0007 - WM_SHOWWINDOW = 0x0018 - WM_SIZE = 0x0005 - WM_STYLECHANGED = 0x007D - WM_SYSKEYDOWN = 0x0104 - WM_SYSKEYUP = 0x0105 - WM_TIMER = 0x0113 - WM_UNICHAR = 0x0109 - WM_USER = 0x0400 - WM_WINDOWPOSCHANGED = 0x0047 - - WS_CLIPCHILDREN = 0x02000000 - WS_CLIPSIBLINGS = 0x04000000 - WS_MAXIMIZE = 0x01000000 - WS_ICONIC = 0x20000000 - WS_VISIBLE = 0x10000000 - WS_OVERLAPPED = 0x00000000 - WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | - WS_MINIMIZEBOX | WS_MAXIMIZEBOX - WS_CAPTION = 0x00C00000 - WS_SYSMENU = 0x00080000 - WS_THICKFRAME = 0x00040000 - WS_MINIMIZEBOX = 0x00020000 - WS_MAXIMIZEBOX = 0x00010000 - - WS_EX_APPWINDOW = 0x00040000 - WS_EX_WINDOWEDGE = 0x00000100 - - QS_ALLINPUT = 0x04FF - - MWMO_WAITALL = 0x0001 - MWMO_INPUTAVAILABLE = 0x0004 - - WAIT_OBJECT_0 = 0 - - PM_REMOVE = 0x0001 - PM_NOREMOVE = 0x0000 - - GHND = 0x0042 - - CF_UNICODETEXT = 13 - IMAGE_BITMAP = 0 - IMAGE_ICON = 1 - IMAGE_CURSOR = 2 - - LR_CREATEDIBSECTION = 0x00002000 - LR_DEFAULTCOLOR = 0x00000000 - LR_DEFAULTSIZE = 0x00000040 - LR_LOADFROMFILE = 0x00000010 - LR_LOADMAP3DCOLORS = 0x00001000 - LR_LOADTRANSPARENT = 0x00000020 - LR_MONOCHROME = 0x00000001 - LR_SHARED = 0x00008000 - LR_VGACOLOR = 0x00000080 -) - -var ( - kernel32 = syscall.NewLazySystemDLL("kernel32.dll") - _GetModuleHandleW = kernel32.NewProc("GetModuleHandleW") - _GlobalAlloc = kernel32.NewProc("GlobalAlloc") - _GlobalFree = kernel32.NewProc("GlobalFree") - _GlobalLock = kernel32.NewProc("GlobalLock") - _GlobalUnlock = kernel32.NewProc("GlobalUnlock") - - user32 = syscall.NewLazySystemDLL("user32.dll") - _AdjustWindowRectEx = user32.NewProc("AdjustWindowRectEx") - _CallMsgFilter = user32.NewProc("CallMsgFilterW") - _CloseClipboard = user32.NewProc("CloseClipboard") - _CreateWindowEx = user32.NewProc("CreateWindowExW") - _DefWindowProc = user32.NewProc("DefWindowProcW") - _DestroyWindow = user32.NewProc("DestroyWindow") - _DispatchMessage = user32.NewProc("DispatchMessageW") - _EmptyClipboard = user32.NewProc("EmptyClipboard") - _EnableMouseInPointer = user32.NewProc("EnableMouseInPointer") - _GetWindowRect = user32.NewProc("GetWindowRect") - _GetClientRect = user32.NewProc("GetClientRect") - _GetClipboardData = user32.NewProc("GetClipboardData") - _GetDC = user32.NewProc("GetDC") - _GetDpiForWindow = user32.NewProc("GetDpiForWindow") - _GetKeyState = user32.NewProc("GetKeyState") - _GetMessage = user32.NewProc("GetMessageW") - _GetMessageTime = user32.NewProc("GetMessageTime") - _GetMonitorInfo = user32.NewProc("GetMonitorInfoW") - _GetPointerInfo = user32.NewProc("GetPointerInfo") - _GetSystemMetrics = user32.NewProc("GetSystemMetrics") - _GetWindowLong = user32.NewProc("GetWindowLongPtrW") - _GetWindowLong32 = user32.NewProc("GetWindowLongW") - _GetWindowPlacement = user32.NewProc("GetWindowPlacement") - _KillTimer = user32.NewProc("KillTimer") - _LoadCursor = user32.NewProc("LoadCursorW") - _LoadImage = user32.NewProc("LoadImageW") - _MonitorFromPoint = user32.NewProc("MonitorFromPoint") - _MonitorFromWindow = user32.NewProc("MonitorFromWindow") - _MoveWindow = user32.NewProc("MoveWindow") - _MsgWaitForMultipleObjectsEx = user32.NewProc("MsgWaitForMultipleObjectsEx") - _OpenClipboard = user32.NewProc("OpenClipboard") - _PeekMessage = user32.NewProc("PeekMessageW") - _PostMessage = user32.NewProc("PostMessageW") - _PostQuitMessage = user32.NewProc("PostQuitMessage") - _ReleaseCapture = user32.NewProc("ReleaseCapture") - _RegisterClassExW = user32.NewProc("RegisterClassExW") - _RegisterTouchWindow = user32.NewProc("RegisterTouchWindow") - _ReleaseDC = user32.NewProc("ReleaseDC") - _ScreenToClient = user32.NewProc("ScreenToClient") - _ShowWindow = user32.NewProc("ShowWindow") - _SetCapture = user32.NewProc("SetCapture") - _SetCursor = user32.NewProc("SetCursor") - _SetClipboardData = user32.NewProc("SetClipboardData") - _SetForegroundWindow = user32.NewProc("SetForegroundWindow") - _SetFocus = user32.NewProc("SetFocus") - _SetProcessDPIAware = user32.NewProc("SetProcessDPIAware") - _SetTimer = user32.NewProc("SetTimer") - _SetWindowLong = user32.NewProc("SetWindowLongPtrW") - _SetWindowLong32 = user32.NewProc("SetWindowLongW") - _SetWindowPlacement = user32.NewProc("SetWindowPlacement") - _SetWindowPos = user32.NewProc("SetWindowPos") - _SetWindowText = user32.NewProc("SetWindowTextW") - _TranslateMessage = user32.NewProc("TranslateMessage") - _UnregisterClass = user32.NewProc("UnregisterClassW") - _UpdateWindow = user32.NewProc("UpdateWindow") - - shcore = syscall.NewLazySystemDLL("shcore") - _GetDpiForMonitor = shcore.NewProc("GetDpiForMonitor") - - gdi32 = syscall.NewLazySystemDLL("gdi32") - _GetDeviceCaps = gdi32.NewProc("GetDeviceCaps") - - imm32 = syscall.NewLazySystemDLL("imm32") - _ImmGetContext = imm32.NewProc("ImmGetContext") - _ImmGetCompositionString = imm32.NewProc("ImmGetCompositionStringW") - _ImmNotifyIME = imm32.NewProc("ImmNotifyIME") - _ImmReleaseContext = imm32.NewProc("ImmReleaseContext") - _ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow") - _ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow") - - dwmapi = syscall.NewLazySystemDLL("dwmapi") - _DwmExtendFrameIntoClientArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea") -) - -func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) { - _AdjustWindowRectEx.Call(uintptr(unsafe.Pointer(r)), uintptr(dwStyle), uintptr(bMenu), uintptr(dwExStyle)) -} - -func CallMsgFilter(m *Msg, nCode uintptr) bool { - r, _, _ := _CallMsgFilter.Call(uintptr(unsafe.Pointer(m)), nCode) - return r != 0 -} - -func CloseClipboard() error { - r, _, err := _CloseClipboard.Call() - if r == 0 { - return fmt.Errorf("CloseClipboard: %v", err) - } - return nil -} - -func CreateWindowEx(dwExStyle uint32, lpClassName uint16, lpWindowName string, dwStyle uint32, x, y, w, h int32, hWndParent, hMenu, hInstance syscall.Handle, lpParam uintptr) (syscall.Handle, error) { - wname := syscall.StringToUTF16Ptr(lpWindowName) - hwnd, _, err := _CreateWindowEx.Call( - uintptr(dwExStyle), - uintptr(lpClassName), - uintptr(unsafe.Pointer(wname)), - uintptr(dwStyle), - uintptr(x), uintptr(y), - uintptr(w), uintptr(h), - uintptr(hWndParent), - uintptr(hMenu), - uintptr(hInstance), - uintptr(lpParam)) - if hwnd == 0 { - return 0, fmt.Errorf("CreateWindowEx failed: %v", err) - } - return syscall.Handle(hwnd), nil -} - -func GetPointerInfo(pointerId uint32) (PointerInfo, error) { - var info PointerInfo - r1, _, err := _GetPointerInfo.Call(uintptr(pointerId), uintptr(unsafe.Pointer(&info))) - if r1 == 0 { - return PointerInfo{}, fmt.Errorf("GetPointerInfo failed: %v", err) - } - return info, nil -} - -func RegisterTouchWindow(hwnd syscall.Handle, flags uint32) error { - r1, _, err := _RegisterTouchWindow.Call(uintptr(hwnd), uintptr(flags)) - if r1 == 0 { - return fmt.Errorf("RegisterTouchWindow failed: %v", err) - } - return nil -} - -func EnableMouseInPointer(enable uint) error { - r1, _, err := _EnableMouseInPointer.Call(uintptr(enable)) - if r1 == 0 { - return fmt.Errorf("EnableMouseInPointer failed: %v", err) - } - return nil -} - -func DefWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr { - r, _, _ := _DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam) - return r -} - -func DestroyWindow(hwnd syscall.Handle) { - _DestroyWindow.Call(uintptr(hwnd)) -} - -func DispatchMessage(m *Msg) { - _DispatchMessage.Call(uintptr(unsafe.Pointer(m))) -} - -func DwmExtendFrameIntoClientArea(hwnd syscall.Handle, margins Margins) error { - r, _, _ := _DwmExtendFrameIntoClientArea.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&margins))) - if r != 0 { - return fmt.Errorf("DwmExtendFrameIntoClientArea: %#x", r) - } - return nil -} - -func EmptyClipboard() error { - r, _, err := _EmptyClipboard.Call() - if r == 0 { - return fmt.Errorf("EmptyClipboard: %v", err) - } - return nil -} - -func GetWindowRect(hwnd syscall.Handle) Rect { - var r Rect - _GetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r))) - return r -} - -func GetClientRect(hwnd syscall.Handle) Rect { - var r Rect - _GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r))) - return r -} - -func GetClipboardData(format uint32) (syscall.Handle, error) { - r, _, err := _GetClipboardData.Call(uintptr(format)) - if r == 0 { - return 0, fmt.Errorf("GetClipboardData: %v", err) - } - return syscall.Handle(r), nil -} - -func GetDC(hwnd syscall.Handle) (syscall.Handle, error) { - hdc, _, err := _GetDC.Call(uintptr(hwnd)) - if hdc == 0 { - return 0, fmt.Errorf("GetDC failed: %v", err) - } - return syscall.Handle(hdc), nil -} - -func GetModuleHandle() (syscall.Handle, error) { - h, _, err := _GetModuleHandleW.Call(uintptr(0)) - if h == 0 { - return 0, fmt.Errorf("GetModuleHandleW failed: %v", err) - } - return syscall.Handle(h), nil -} - -func getDeviceCaps(hdc syscall.Handle, index int32) int { - c, _, _ := _GetDeviceCaps.Call(uintptr(hdc), uintptr(index)) - return int(c) -} - -func getDpiForMonitor(hmonitor syscall.Handle, dpiType uint32) int { - var dpiX, dpiY uintptr - _GetDpiForMonitor.Call(uintptr(hmonitor), uintptr(dpiType), uintptr(unsafe.Pointer(&dpiX)), uintptr(unsafe.Pointer(&dpiY))) - return int(dpiX) -} - -// GetSystemDPI returns the effective DPI of the system. -func GetSystemDPI() int { - // Check for GetDpiForMonitor, introduced in Windows 8.1. - if _GetDpiForMonitor.Find() == nil { - hmon := monitorFromPoint(Point{}, MONITOR_DEFAULTTOPRIMARY) - return getDpiForMonitor(hmon, MDT_EFFECTIVE_DPI) - } else { - // Fall back to the physical device DPI. - screenDC, err := GetDC(0) - if err != nil { - return 96 - } - defer ReleaseDC(screenDC) - return getDeviceCaps(screenDC, LOGPIXELSX) - } -} - -func GetKeyState(nVirtKey int32) int16 { - c, _, _ := _GetKeyState.Call(uintptr(nVirtKey)) - return int16(c) -} - -func GetMessage(m *Msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax uint32) int32 { - r, _, _ := _GetMessage.Call(uintptr(unsafe.Pointer(m)), - uintptr(hwnd), - uintptr(wMsgFilterMin), - uintptr(wMsgFilterMax)) - return int32(r) -} - -func GetMessageTime() time.Duration { - r, _, _ := _GetMessageTime.Call() - return time.Duration(r) * time.Millisecond -} - -func GetSystemMetrics(nIndex int) int { - r, _, _ := _GetSystemMetrics.Call(uintptr(nIndex)) - return int(r) -} - -// GetWindowDPI returns the effective DPI of the window. -func GetWindowDPI(hwnd syscall.Handle) int { - // Check for GetDpiForWindow, introduced in Windows 10. - if _GetDpiForWindow.Find() == nil { - dpi, _, _ := _GetDpiForWindow.Call(uintptr(hwnd)) - return int(dpi) - } else { - return GetSystemDPI() - } -} - -func GetWindowPlacement(hwnd syscall.Handle) *WindowPlacement { - var wp WindowPlacement - wp.length = uint32(unsafe.Sizeof(wp)) - _GetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&wp))) - return &wp -} - -func GetMonitorInfo(hwnd syscall.Handle) MonitorInfo { - var mi MonitorInfo - mi.cbSize = uint32(unsafe.Sizeof(mi)) - v, _, _ := _MonitorFromWindow.Call(uintptr(hwnd), MONITOR_DEFAULTTOPRIMARY) - _GetMonitorInfo.Call(v, uintptr(unsafe.Pointer(&mi))) - return mi -} - -func GetWindowLong(hwnd syscall.Handle, index uintptr) (val uintptr) { - if runtime.GOARCH == "386" { - val, _, _ = _GetWindowLong32.Call(uintptr(hwnd), index) - } else { - val, _, _ = _GetWindowLong.Call(uintptr(hwnd), index) - } - return -} - -func ImmGetContext(hwnd syscall.Handle) syscall.Handle { - h, _, _ := _ImmGetContext.Call(uintptr(hwnd)) - return syscall.Handle(h) -} - -func ImmReleaseContext(hwnd, imc syscall.Handle) { - _ImmReleaseContext.Call(uintptr(hwnd), uintptr(imc)) -} - -func ImmNotifyIME(imc syscall.Handle, action, index, value int) { - _ImmNotifyIME.Call(uintptr(imc), uintptr(action), uintptr(index), uintptr(value)) -} - -func ImmGetCompositionString(imc syscall.Handle, key int) string { - size, _, _ := _ImmGetCompositionString.Call(uintptr(imc), uintptr(key), 0, 0) - if int32(size) <= 0 { - return "" - } - u16 := make([]uint16, size/unsafe.Sizeof(uint16(0))) - _ImmGetCompositionString.Call(uintptr(imc), uintptr(key), uintptr(unsafe.Pointer(&u16[0])), size) - return string(utf16.Decode(u16)) -} - -func ImmGetCompositionValue(imc syscall.Handle, key int) int { - val, _, _ := _ImmGetCompositionString.Call(uintptr(imc), uintptr(key), 0, 0) - return int(int32(val)) -} - -func ImmSetCompositionWindow(imc syscall.Handle, x, y int) { - f := CompositionForm{ - dwStyle: CFS_POINT, - ptCurrentPos: Point{ - X: int32(x), Y: int32(y), - }, - } - _ImmSetCompositionWindow.Call(uintptr(imc), uintptr(unsafe.Pointer(&f))) -} - -func ImmSetCandidateWindow(imc syscall.Handle, x, y int) { - f := CandidateForm{ - dwStyle: CFS_CANDIDATEPOS, - ptCurrentPos: Point{ - X: int32(x), Y: int32(y), - }, - } - _ImmSetCandidateWindow.Call(uintptr(imc), uintptr(unsafe.Pointer(&f))) -} - -func SetWindowLong(hwnd syscall.Handle, idx uintptr, style uintptr) { - if runtime.GOARCH == "386" { - _SetWindowLong32.Call(uintptr(hwnd), idx, style) - } else { - _SetWindowLong.Call(uintptr(hwnd), idx, style) - } -} - -func SetWindowPlacement(hwnd syscall.Handle, wp *WindowPlacement) { - _SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp))) -} - -func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int32, style uintptr) { - _SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter), - uintptr(x), uintptr(y), - uintptr(dx), uintptr(dy), - style, - ) -} - -func SetWindowText(hwnd syscall.Handle, title string) { - wname := syscall.StringToUTF16Ptr(title) - _SetWindowText.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wname))) -} - -func GlobalAlloc(size int) (syscall.Handle, error) { - r, _, err := _GlobalAlloc.Call(GHND, uintptr(size)) - if r == 0 { - return 0, fmt.Errorf("GlobalAlloc: %v", err) - } - return syscall.Handle(r), nil -} - -func GlobalFree(h syscall.Handle) { - _GlobalFree.Call(uintptr(h)) -} - -func GlobalLock(h syscall.Handle) (unsafe.Pointer, error) { - r, _, err := _GlobalLock.Call(uintptr(h)) - if r == 0 { - return nil, fmt.Errorf("GlobalLock: %v", err) - } - return unsafe.Pointer(r), nil -} - -func GlobalUnlock(h syscall.Handle) { - _GlobalUnlock.Call(uintptr(h)) -} - -func KillTimer(hwnd syscall.Handle, nIDEvent uintptr) error { - r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), 0, 0) - if r == 0 { - return fmt.Errorf("KillTimer failed: %v", err) - } - return nil -} - -func LoadCursor(curID uint16) (syscall.Handle, error) { - h, _, err := _LoadCursor.Call(0, uintptr(curID)) - if h == 0 { - return 0, fmt.Errorf("LoadCursorW failed: %v", err) - } - return syscall.Handle(h), nil -} - -func LoadImage(hInst syscall.Handle, res uint32, typ uint32, cx, cy int, fuload uint32) (syscall.Handle, error) { - h, _, err := _LoadImage.Call(uintptr(hInst), uintptr(res), uintptr(typ), uintptr(cx), uintptr(cy), uintptr(fuload)) - if h == 0 { - return 0, fmt.Errorf("LoadImageW failed: %v", err) - } - return syscall.Handle(h), nil -} - -func MoveWindow(hwnd syscall.Handle, x, y, width, height int32, repaint bool) { - var paint uintptr - if repaint { - paint = TRUE - } - _MoveWindow.Call(uintptr(hwnd), uintptr(x), uintptr(y), uintptr(width), uintptr(height), paint) -} - -func monitorFromPoint(pt Point, flags uint32) syscall.Handle { - r, _, _ := _MonitorFromPoint.Call(uintptr(pt.X), uintptr(pt.Y), uintptr(flags)) - return syscall.Handle(r) -} - -func MsgWaitForMultipleObjectsEx(nCount uint32, pHandles uintptr, millis, mask, flags uint32) (uint32, error) { - r, _, err := _MsgWaitForMultipleObjectsEx.Call(uintptr(nCount), pHandles, uintptr(millis), uintptr(mask), uintptr(flags)) - res := uint32(r) - if res == 0xFFFFFFFF { - return 0, fmt.Errorf("MsgWaitForMultipleObjectsEx failed: %v", err) - } - return res, nil -} - -func OpenClipboard(hwnd syscall.Handle) error { - r, _, err := _OpenClipboard.Call(uintptr(hwnd)) - if r == 0 { - return fmt.Errorf("OpenClipboard: %v", err) - } - return nil -} - -func PeekMessage(m *Msg, hwnd syscall.Handle, wMsgFilterMin, wMsgFilterMax, wRemoveMsg uint32) bool { - r, _, _ := _PeekMessage.Call(uintptr(unsafe.Pointer(m)), uintptr(hwnd), uintptr(wMsgFilterMin), uintptr(wMsgFilterMax), uintptr(wRemoveMsg)) - return r != 0 -} - -func PostQuitMessage(exitCode uintptr) { - _PostQuitMessage.Call(exitCode) -} - -func PostMessage(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) error { - r, _, err := _PostMessage.Call(uintptr(hwnd), uintptr(msg), wParam, lParam) - if r == 0 { - return fmt.Errorf("PostMessage failed: %v", err) - } - return nil -} - -func ReleaseCapture() bool { - r, _, _ := _ReleaseCapture.Call() - return r != 0 -} - -func RegisterClassEx(cls *WndClassEx) (uint16, error) { - a, _, err := _RegisterClassExW.Call(uintptr(unsafe.Pointer(cls))) - if a == 0 { - return 0, fmt.Errorf("RegisterClassExW failed: %v", err) - } - return uint16(a), nil -} - -func ReleaseDC(hdc syscall.Handle) { - _ReleaseDC.Call(uintptr(hdc)) -} - -func SetForegroundWindow(hwnd syscall.Handle) { - _SetForegroundWindow.Call(uintptr(hwnd)) -} - -func SetFocus(hwnd syscall.Handle) { - _SetFocus.Call(uintptr(hwnd)) -} - -func SetProcessDPIAware() { - _SetProcessDPIAware.Call() -} - -func SetCapture(hwnd syscall.Handle) syscall.Handle { - r, _, _ := _SetCapture.Call(uintptr(hwnd)) - return syscall.Handle(r) -} - -func SetClipboardData(format uint32, mem syscall.Handle) error { - r, _, err := _SetClipboardData.Call(uintptr(format), uintptr(mem)) - if r == 0 { - return fmt.Errorf("SetClipboardData: %v", err) - } - return nil -} - -func SetCursor(h syscall.Handle) { - _SetCursor.Call(uintptr(h)) -} - -func SetTimer(hwnd syscall.Handle, nIDEvent uintptr, uElapse uint32, timerProc uintptr) error { - r, _, err := _SetTimer.Call(uintptr(hwnd), uintptr(nIDEvent), uintptr(uElapse), timerProc) - if r == 0 { - return fmt.Errorf("SetTimer failed: %v", err) - } - return nil -} - -func ScreenToClient(hwnd syscall.Handle, p *Point) { - _ScreenToClient.Call(uintptr(hwnd), uintptr(unsafe.Pointer(p))) -} - -func ShowWindow(hwnd syscall.Handle, nCmdShow int32) { - _ShowWindow.Call(uintptr(hwnd), uintptr(nCmdShow)) -} - -func TranslateMessage(m *Msg) { - _TranslateMessage.Call(uintptr(unsafe.Pointer(m))) -} - -func UnregisterClass(cls uint16, hInst syscall.Handle) { - _UnregisterClass.Call(uintptr(cls), uintptr(hInst)) -} - -func UpdateWindow(hwnd syscall.Handle) { - _UpdateWindow.Call(uintptr(hwnd)) -} - -func (p WindowPlacement) Rect() Rect { - return p.rcNormalPosition -} - -func (p WindowPlacement) IsMinimized() bool { - return p.showCmd == SW_SHOWMINIMIZED -} - -func (p WindowPlacement) IsMaximized() bool { - return p.showCmd == SW_SHOWMAXIMIZED -} - -func (p *WindowPlacement) Set(Left, Top, Right, Bottom int) { - p.rcNormalPosition.Left = int32(Left) - p.rcNormalPosition.Top = int32(Top) - p.rcNormalPosition.Right = int32(Right) - p.rcNormalPosition.Bottom = int32(Bottom) -} diff --git a/gio/app/internal/xkb/xkb_unix.go b/gio/app/internal/xkb/xkb_unix.go deleted file mode 100644 index a5835b1..0000000 --- a/gio/app/internal/xkb/xkb_unix.go +++ /dev/null @@ -1,375 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build (linux && !android) || freebsd || openbsd -// +build linux,!android freebsd openbsd - -// Package xkb implements a Go interface for the X Keyboard Extension library. -package xkb - -import ( - "errors" - "fmt" - "os" - "syscall" - "unicode" - "unicode/utf8" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" -) - -/* -#cgo linux pkg-config: xkbcommon -#cgo freebsd openbsd CFLAGS: -I/usr/local/include -#cgo freebsd openbsd LDFLAGS: -L/usr/local/lib -lxkbcommon - -#include -#include -#include -*/ -import "C" - -type Context struct { - Ctx *C.struct_xkb_context - keyMap *C.struct_xkb_keymap - state *C.struct_xkb_state - compTable *C.struct_xkb_compose_table - compState *C.struct_xkb_compose_state - utf8Buf []byte -} - -var ( - _XKB_MOD_NAME_CTRL = []byte("Control\x00") - _XKB_MOD_NAME_SHIFT = []byte("Shift\x00") - _XKB_MOD_NAME_ALT = []byte("Mod1\x00") - _XKB_MOD_NAME_LOGO = []byte("Mod4\x00") -) - -func (x *Context) Destroy() { - if x.compState != nil { - C.xkb_compose_state_unref(x.compState) - x.compState = nil - } - if x.compTable != nil { - C.xkb_compose_table_unref(x.compTable) - x.compTable = nil - } - x.DestroyKeymapState() - if x.Ctx != nil { - C.xkb_context_unref(x.Ctx) - x.Ctx = nil - } -} - -func New() (*Context, error) { - ctx := &Context{ - Ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS), - } - if ctx.Ctx == nil { - return nil, errors.New("newXKB: xkb_context_new failed") - } - locale := os.Getenv("LC_ALL") - if locale == "" { - locale = os.Getenv("LC_CTYPE") - } - if locale == "" { - locale = os.Getenv("LANG") - } - if locale == "" { - locale = "C" - } - cloc := C.CString(locale) - defer C.free(unsafe.Pointer(cloc)) - ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.Ctx, cloc, C.XKB_COMPOSE_COMPILE_NO_FLAGS) - if ctx.compTable == nil { - ctx.Destroy() - return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed") - } - ctx.compState = C.xkb_compose_state_new(ctx.compTable, C.XKB_COMPOSE_STATE_NO_FLAGS) - if ctx.compState == nil { - ctx.Destroy() - return nil, errors.New("newXKB: xkb_compose_state_new failed") - } - return ctx, nil -} - -func (x *Context) DestroyKeymapState() { - if x.state != nil { - C.xkb_state_unref(x.state) - x.state = nil - } - if x.keyMap != nil { - C.xkb_keymap_unref(x.keyMap) - x.keyMap = nil - } -} - -// SetKeymap sets the keymap and state. The context takes ownership of the -// keymap and state and frees them in Destroy. -func (x *Context) SetKeymap(xkbKeyMap, xkbState unsafe.Pointer) { - x.DestroyKeymapState() - x.keyMap = (*C.struct_xkb_keymap)(xkbKeyMap) - x.state = (*C.struct_xkb_state)(xkbState) -} - -func (x *Context) LoadKeymap(format int, fd int, size int) error { - x.DestroyKeymapState() - mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED) - if err != nil { - return fmt.Errorf("newXKB: mmap of keymap failed: %v", err) - } - defer syscall.Munmap(mapData) - keyMap := C.xkb_keymap_new_from_buffer(x.Ctx, (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS) - if keyMap == nil { - return errors.New("newXKB: xkb_keymap_new_from_buffer failed") - } - state := C.xkb_state_new(keyMap) - if state == nil { - C.xkb_keymap_unref(keyMap) - return errors.New("newXKB: xkb_state_new failed") - } - x.keyMap = keyMap - x.state = state - return nil -} - -func (x *Context) Modifiers() key.Modifiers { - var mods key.Modifiers - if x.state == nil { - return mods - } - - if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 { - mods |= key.ModCtrl - } - if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_SHIFT[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 { - mods |= key.ModShift - } - if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_ALT[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 { - mods |= key.ModAlt - } - if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_LOGO[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 { - mods |= key.ModSuper - } - return mods -} - -func (x *Context) DispatchKey(keyCode uint32, state key.State) (events []event.Event) { - if x.state == nil { - return - } - kc := C.xkb_keycode_t(keyCode) - if len(x.utf8Buf) == 0 { - x.utf8Buf = make([]byte, 1) - } - sym := C.xkb_state_key_get_one_sym(x.state, kc) - if name, ok := convertKeysym(sym); ok { - cmd := key.Event{ - Name: name, - Modifiers: x.Modifiers(), - State: state, - } - // Ensure that a physical backtab key is translated to - // Shift-Tab. - if sym == C.XKB_KEY_ISO_Left_Tab { - cmd.Modifiers |= key.ModShift - } - events = append(events, cmd) - } - C.xkb_compose_state_feed(x.compState, sym) - var str []byte - switch C.xkb_compose_state_get_status(x.compState) { - case C.XKB_COMPOSE_CANCELLED, C.XKB_COMPOSE_COMPOSING: - return - case C.XKB_COMPOSE_COMPOSED: - size := C.xkb_compose_state_get_utf8(x.compState, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) - if int(size) >= len(x.utf8Buf) { - x.utf8Buf = make([]byte, size+1) - size = C.xkb_compose_state_get_utf8(x.compState, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) - } - C.xkb_compose_state_reset(x.compState) - str = x.utf8Buf[:size] - case C.XKB_COMPOSE_NOTHING: - mod := x.Modifiers() - if mod&(key.ModCtrl|key.ModAlt|key.ModSuper) == 0 { - str = x.charsForKeycode(kc) - } - } - // Report only printable runes. - var n int - for n < len(str) { - r, s := utf8.DecodeRune(str) - if unicode.IsPrint(r) { - n += s - } else { - copy(str[n:], str[n+s:]) - str = str[:len(str)-s] - } - } - if state == key.Press && len(str) > 0 { - events = append(events, key.EditEvent{Text: string(str)}) - } - return -} - -func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte { - size := C.xkb_state_key_get_utf8(x.state, keyCode, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) - if int(size) >= len(x.utf8Buf) { - x.utf8Buf = make([]byte, size+1) - size = C.xkb_state_key_get_utf8(x.state, keyCode, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf))) - } - return x.utf8Buf[:size] -} - -func (x *Context) IsRepeatKey(keyCode uint32) bool { - if x.state == nil { - return false - } - kc := C.xkb_keycode_t(keyCode) - return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1 -} - -func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latchedGroup, lockedGroup uint32) { - if x.state == nil { - return - } - C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed), C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked), - C.xkb_layout_index_t(depressedGroup), C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup)) -} - -func convertKeysym(s C.xkb_keysym_t) (key.Name, bool) { - if 'a' <= s && s <= 'z' { - return key.Name(rune(s - 'a' + 'A')), true - } - if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 { - return key.Name(rune(s - C.XKB_KEY_KP_0 + '0')), true - } - if ' ' < s && s <= '~' { - return key.Name(rune(s)), true - } - var n key.Name - switch s { - case C.XKB_KEY_Escape: - n = key.NameEscape - case C.XKB_KEY_Left: - n = key.NameLeftArrow - case C.XKB_KEY_Right: - n = key.NameRightArrow - case C.XKB_KEY_Return: - n = key.NameReturn - case C.XKB_KEY_Up: - n = key.NameUpArrow - case C.XKB_KEY_Down: - n = key.NameDownArrow - case C.XKB_KEY_Home: - n = key.NameHome - case C.XKB_KEY_End: - n = key.NameEnd - case C.XKB_KEY_BackSpace: - n = key.NameDeleteBackward - case C.XKB_KEY_Delete: - n = key.NameDeleteForward - case C.XKB_KEY_Page_Up: - n = key.NamePageUp - case C.XKB_KEY_Page_Down: - n = key.NamePageDown - case C.XKB_KEY_F1: - n = key.NameF1 - case C.XKB_KEY_F2: - n = key.NameF2 - case C.XKB_KEY_F3: - n = key.NameF3 - case C.XKB_KEY_F4: - n = key.NameF4 - case C.XKB_KEY_F5: - n = key.NameF5 - case C.XKB_KEY_F6: - n = key.NameF6 - case C.XKB_KEY_F7: - n = key.NameF7 - case C.XKB_KEY_F8: - n = key.NameF8 - case C.XKB_KEY_F9: - n = key.NameF9 - case C.XKB_KEY_F10: - n = key.NameF10 - case C.XKB_KEY_F11: - n = key.NameF11 - case C.XKB_KEY_F12: - n = key.NameF12 - case C.XKB_KEY_Tab, C.XKB_KEY_ISO_Left_Tab: - n = key.NameTab - case 0x20: - n = key.NameSpace - case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R: - n = key.NameCtrl - case C.XKB_KEY_Shift_L, C.XKB_KEY_Shift_R: - n = key.NameShift - case C.XKB_KEY_Alt_L, C.XKB_KEY_Alt_R: - n = key.NameAlt - case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R: - n = key.NameSuper - - case C.XKB_KEY_KP_Space: - n = key.NameSpace - case C.XKB_KEY_KP_Tab: - n = key.NameTab - case C.XKB_KEY_KP_Enter: - n = key.NameEnter - case C.XKB_KEY_KP_F1: - n = key.NameF1 - case C.XKB_KEY_KP_F2: - n = key.NameF2 - case C.XKB_KEY_KP_F3: - n = key.NameF3 - case C.XKB_KEY_KP_F4: - n = key.NameF4 - case C.XKB_KEY_KP_Home: - n = key.NameHome - case C.XKB_KEY_KP_Left: - n = key.NameLeftArrow - case C.XKB_KEY_KP_Up: - n = key.NameUpArrow - case C.XKB_KEY_KP_Right: - n = key.NameRightArrow - case C.XKB_KEY_KP_Down: - n = key.NameDownArrow - case C.XKB_KEY_KP_Prior: - // not supported - return "", false - case C.XKB_KEY_KP_Next: - // not supported - return "", false - case C.XKB_KEY_KP_End: - n = key.NameEnd - case C.XKB_KEY_KP_Begin: - n = key.NameHome - case C.XKB_KEY_KP_Insert: - // not supported - return "", false - case C.XKB_KEY_KP_Delete: - n = key.NameDeleteForward - case C.XKB_KEY_KP_Multiply: - n = "*" - case C.XKB_KEY_KP_Add: - n = "+" - case C.XKB_KEY_KP_Separator: - // not supported - return "", false - case C.XKB_KEY_KP_Subtract: - n = "-" - case C.XKB_KEY_KP_Decimal: - // TODO(dh): does a German keyboard layout also translate the numpad key to XKB_KEY_KP_DECIMAL? Because in - // German, the decimal is a comma, not a period. - n = "." - case C.XKB_KEY_KP_Divide: - n = "/" - case C.XKB_KEY_KP_Equal: - n = "=" - - default: - return "", false - } - return n, true -} diff --git a/gio/app/log_android.go b/gio/app/log_android.go deleted file mode 100644 index 6318b08..0000000 --- a/gio/app/log_android.go +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -/* -#cgo LDFLAGS: -llog - -#include -#include -*/ -import "C" - -import ( - "bufio" - "log" - "os" - "runtime" - "syscall" - "unsafe" -) - -// 1024 is the truncation limit from android/log.h, plus a \n. -const logLineLimit = 1024 - -var logTag = C.CString(ID) - -func init() { - // Android's logcat already includes timestamps. - log.SetFlags(log.Flags() &^ log.LstdFlags) - log.SetOutput(new(androidLogWriter)) - - // Redirect stdout and stderr to the Android logger. - logFd(os.Stdout.Fd()) - logFd(os.Stderr.Fd()) -} - -type androidLogWriter struct { - // buf has room for the maximum log line, plus a terminating '\0'. - buf [logLineLimit + 1]byte -} - -func (w *androidLogWriter) Write(data []byte) (int, error) { - n := 0 - for len(data) > 0 { - msg := data - // Truncate the buffer, leaving space for the '\0'. - if max := len(w.buf) - 1; len(msg) > max { - msg = msg[:max] - } - buf := w.buf[:len(msg)+1] - copy(buf, msg) - // Terminating '\0'. - buf[len(msg)] = 0 - C.__android_log_write(C.ANDROID_LOG_INFO, logTag, (*C.char)(unsafe.Pointer(&buf[0]))) - n += len(msg) - data = data[len(msg):] - } - return n, nil -} - -func logFd(fd uintptr) { - r, w, err := os.Pipe() - if err != nil { - panic(err) - } - if err := syscall.Dup3(int(w.Fd()), int(fd), syscall.O_CLOEXEC); err != nil { - panic(err) - } - go func() { - lineBuf := bufio.NewReaderSize(r, logLineLimit) - // The buffer to pass to C, including the terminating '\0'. - buf := make([]byte, lineBuf.Size()+1) - cbuf := (*C.char)(unsafe.Pointer(&buf[0])) - for { - line, _, err := lineBuf.ReadLine() - if err != nil { - break - } - copy(buf, line) - buf[len(line)] = 0 - C.__android_log_write(C.ANDROID_LOG_INFO, logTag, cbuf) - } - // The garbage collector doesn't know that w's fd was dup'ed. - // Avoid finalizing w, and thereby avoid its finalizer closing its fd. - runtime.KeepAlive(w) - }() -} diff --git a/gio/app/log_ios.go b/gio/app/log_ios.go deleted file mode 100644 index 4f12c05..0000000 --- a/gio/app/log_ios.go +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build darwin && ios -// +build darwin,ios - -package app - -/* -#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c - -@import Foundation; - -static void nslog(char *str) { - NSLog(@"%@", @(str)); -} -*/ -import "C" - -import ( - "bufio" - "io" - "log" - "unsafe" - - _ "github.com/p9c/p9/pkg/gel/gio/internal/cocoainit" -) - -func init() { - // macOS Console already includes timestamps. - log.SetFlags(log.Flags() &^ log.LstdFlags) - log.SetOutput(newNSLogWriter()) -} - -func newNSLogWriter() io.Writer { - r, w := io.Pipe() - go func() { - // 1024 is an arbitrary truncation limit, taken from Android's - // log buffer size. - lineBuf := bufio.NewReaderSize(r, 1024) - // The buffer to pass to C, including the terminating '\0'. - buf := make([]byte, lineBuf.Size()+1) - cbuf := (*C.char)(unsafe.Pointer(&buf[0])) - for { - line, _, err := lineBuf.ReadLine() - if err != nil { - break - } - copy(buf, line) - buf[len(line)] = 0 - C.nslog(cbuf) - } - }() - return w -} diff --git a/gio/app/log_windows.go b/gio/app/log_windows.go deleted file mode 100644 index 17de291..0000000 --- a/gio/app/log_windows.go +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "log" - "unsafe" - - syscall "golang.org/x/sys/windows" -) - -type logger struct{} - -var ( - kernel32 = syscall.NewLazySystemDLL("kernel32") - outputDebugStringW = kernel32.NewProc("OutputDebugStringW") - debugView *logger -) - -func init() { - // Windows DebugView already includes timestamps. - if syscall.Stderr == 0 { - log.SetFlags(log.Flags() &^ log.LstdFlags) - log.SetOutput(debugView) - } -} - -func (l *logger) Write(buf []byte) (int, error) { - p, err := syscall.UTF16PtrFromString(string(buf)) - if err != nil { - return 0, err - } - outputDebugStringW.Call(uintptr(unsafe.Pointer(p))) - return len(buf), nil -} diff --git a/gio/app/metal_darwin.go b/gio/app/metal_darwin.go deleted file mode 100644 index 5e1b1fc..0000000 --- a/gio/app/metal_darwin.go +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build !nometal -// +build !nometal - -package app - -import ( - "errors" - - "github.com/p9c/p9/pkg/gel/gio/gpu" -) - -/* -#cgo CFLAGS: -Werror -xobjective-c -fobjc-arc -#cgo LDFLAGS: -framework QuartzCore -framework Metal - -#import -#import - -#include - -static CFTypeRef createMetalDevice(void) { - @autoreleasepool { - id dev = MTLCreateSystemDefaultDevice(); - return CFBridgingRetain(dev); - } -} - -static void setupLayer(CFTypeRef layerRef, CFTypeRef devRef) { - @autoreleasepool { - CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef; - id dev = (__bridge id)devRef; - layer.device = dev; - // Package gpu assumes an sRGB-encoded framebuffer. - layer.pixelFormat = MTLPixelFormatBGRA8Unorm_sRGB; - if (@available(iOS 11.0, *)) { - // Never let nextDrawable time out and return nil. - layer.allowsNextDrawableTimeout = NO; - } - } -} - -static CFTypeRef nextDrawable(CFTypeRef layerRef) { - @autoreleasepool { - CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef; - return CFBridgingRetain([layer nextDrawable]); - } -} - -static CFTypeRef drawableTexture(CFTypeRef drawableRef) { - @autoreleasepool { - id drawable = (__bridge id)drawableRef; - return CFBridgingRetain(drawable.texture); - } -} - -static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) { - @autoreleasepool { - id drawable = (__bridge id)drawableRef; - id queue = (__bridge id)queueRef; - id cmdBuffer = [queue commandBuffer]; - [cmdBuffer commit]; - [cmdBuffer waitUntilScheduled]; - [drawable present]; - } -} - -static CFTypeRef newCommandQueue(CFTypeRef devRef) { - @autoreleasepool { - id dev = (__bridge id)devRef; - return CFBridgingRetain([dev newCommandQueue]); - } -} -*/ -import "C" - -type mtlContext struct { - dev C.CFTypeRef - view C.CFTypeRef - layer C.CFTypeRef - queue C.CFTypeRef - drawable C.CFTypeRef - texture C.CFTypeRef -} - -func newMtlContext(w *window) (*mtlContext, error) { - dev := C.createMetalDevice() - if dev == 0 { - return nil, errors.New("metal: MTLCreateSystemDefaultDevice failed") - } - view := w.contextView() - layer := getMetalLayer(view) - if layer == 0 { - C.CFRelease(dev) - return nil, errors.New("metal: CAMetalLayer construction failed") - } - queue := C.newCommandQueue(dev) - if queue == 0 { - C.CFRelease(dev) - C.CFRelease(layer) - return nil, errors.New("metal: [MTLDevice newCommandQueue] failed") - } - C.setupLayer(layer, dev) - c := &mtlContext{ - dev: dev, - view: view, - layer: layer, - queue: queue, - } - return c, nil -} - -func (c *mtlContext) RenderTarget() (gpu.RenderTarget, error) { - if c.drawable != 0 || c.texture != 0 { - return nil, errors.New("metal:a previous RenderTarget wasn't Presented") - } - c.drawable = C.nextDrawable(c.layer) - if c.drawable == 0 { - return nil, errors.New("metal: [CAMetalLayer nextDrawable] failed") - } - c.texture = C.drawableTexture(c.drawable) - if c.texture == 0 { - return nil, errors.New("metal: CADrawable.texture is nil") - } - return gpu.MetalRenderTarget{ - Texture: uintptr(c.texture), - }, nil -} - -func (c *mtlContext) API() gpu.API { - return gpu.Metal{ - Device: uintptr(c.dev), - Queue: uintptr(c.queue), - PixelFormat: int(C.MTLPixelFormatBGRA8Unorm_sRGB), - } -} - -func (c *mtlContext) Release() { - C.CFRelease(c.queue) - C.CFRelease(c.dev) - C.CFRelease(c.layer) - if c.drawable != 0 { - C.CFRelease(c.drawable) - } - if c.texture != 0 { - C.CFRelease(c.texture) - } - *c = mtlContext{} -} - -func (c *mtlContext) Present() error { - C.CFRelease(c.texture) - c.texture = 0 - C.presentDrawable(c.queue, c.drawable) - C.CFRelease(c.drawable) - c.drawable = 0 - return nil -} - -func (c *mtlContext) Lock() error { - return nil -} - -func (c *mtlContext) Unlock() {} - -func (c *mtlContext) Refresh() error { - resizeDrawable(c.view, c.layer) - return nil -} - -func (w *window) NewContext() (context, error) { - return newMtlContext(w) -} diff --git a/gio/app/metal_ios.go b/gio/app/metal_ios.go deleted file mode 100644 index d5b5dc4..0000000 --- a/gio/app/metal_ios.go +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build !nometal -// +build !nometal - -package app - -/* -#cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc - -@import UIKit; - -@import QuartzCore.CAMetalLayer; - -#include - -Class gio_layerClass(void) { - return [CAMetalLayer class]; -} - -static CFTypeRef getMetalLayer(CFTypeRef viewRef) { - @autoreleasepool { - UIView *view = (__bridge UIView *)viewRef; - CAMetalLayer *l = (CAMetalLayer *)view.layer; - l.needsDisplayOnBoundsChange = YES; - l.presentsWithTransaction = YES; - return CFBridgingRetain(l); - } -} - -static void resizeDrawable(CFTypeRef viewRef, CFTypeRef layerRef) { - @autoreleasepool { - UIView *view = (__bridge UIView *)viewRef; - CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef; - layer.contentsScale = view.contentScaleFactor; - CGSize size = layer.bounds.size; - size.width *= layer.contentsScale; - size.height *= layer.contentsScale; - layer.drawableSize = size; - } -} -*/ -import "C" - -func getMetalLayer(view C.CFTypeRef) C.CFTypeRef { - return C.getMetalLayer(view) -} - -func resizeDrawable(view, layer C.CFTypeRef) { - C.resizeDrawable(view, layer) -} diff --git a/gio/app/metal_macos.go b/gio/app/metal_macos.go deleted file mode 100644 index 15d60b0..0000000 --- a/gio/app/metal_macos.go +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build darwin && !ios && !nometal -// +build darwin,!ios,!nometal - -package app - -/* -#cgo CFLAGS: -Werror -xobjective-c -fobjc-arc - -#import -#import -#include - -CALayer *gio_layerFactory(BOOL presentWithTrans) { - @autoreleasepool { - CAMetalLayer *l = [CAMetalLayer layer]; - l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable; - l.needsDisplayOnBoundsChange = YES; - l.presentsWithTransaction = presentWithTrans; - return l; - } -} - -static CFTypeRef getMetalLayer(CFTypeRef viewRef) { - @autoreleasepool { - NSView *view = (__bridge NSView *)viewRef; - return CFBridgingRetain(view.layer); - } -} - -static void resizeDrawable(CFTypeRef viewRef, CFTypeRef layerRef) { - @autoreleasepool { - NSView *view = (__bridge NSView *)viewRef; - CAMetalLayer *layer = (__bridge CAMetalLayer *)layerRef; - CGSize size = layer.bounds.size; - size.width *= layer.contentsScale; - size.height *= layer.contentsScale; - layer.drawableSize = size; - } -} -*/ -import "C" - -func getMetalLayer(view C.CFTypeRef) C.CFTypeRef { - return C.getMetalLayer(view) -} - -func resizeDrawable(view, layer C.CFTypeRef) { - C.resizeDrawable(view, layer) -} diff --git a/gio/app/os.go b/gio/app/os.go deleted file mode 100644 index eb806bc..0000000 --- a/gio/app/os.go +++ /dev/null @@ -1,365 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "errors" - "image" - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/op" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// errOutOfDate is reported when the GPU surface dimensions or properties no -// longer match the window. -var errOutOfDate = errors.New("app: GPU surface out of date") - -// Config describes a Window configuration. -type Config struct { - // Size is the window dimensions (Width, Height). - Size image.Point - // MaxSize is the window maximum allowed dimensions. - MaxSize image.Point - // MinSize is the window minimum allowed dimensions. - MinSize image.Point - // Title is the window title displayed in its decoration bar. - Title string - // WindowMode is the window mode. - Mode WindowMode - // StatusColor is the color of the Android status bar. - StatusColor color.NRGBA - // NavigationColor is the color of the navigation bar - // on Android, or the address bar in browsers. - NavigationColor color.NRGBA - // Orientation is the current window orientation. - Orientation Orientation - // CustomRenderer is true when the window content is rendered by the - // client. - CustomRenderer bool - // Decorated reports whether window decorations are provided automatically. - Decorated bool - // Focused reports whether has the keyboard focus. - Focused bool - // decoHeight is the height of the fallback decoration for platforms such - // as Wayland that may need fallback client-side decorations. - decoHeight unit.Dp -} - -// ConfigEvent is sent whenever the configuration of a Window changes. -type ConfigEvent struct { - Config Config -} - -func (c *Config) apply(m unit.Metric, options []Option) { - for _, o := range options { - o(m, c) - } -} - -type wakeupEvent struct{} - -// WindowMode is the window mode (WindowMode.Option sets it). -// Note that mode can be changed programatically as well as by the user -// clicking on the minimize/maximize buttons on the window's title bar. -type WindowMode uint8 - -const ( - // Windowed is the normal window mode with OS specific window decorations. - Windowed WindowMode = iota - // Fullscreen is the full screen window mode. - Fullscreen - // Minimized is for systems where the window can be minimized to an icon. - Minimized - // Maximized is for systems where the window can be made to fill the available monitor area. - Maximized -) - -// Option changes the mode of a Window. -func (m WindowMode) Option() Option { - return func(_ unit.Metric, cnf *Config) { - cnf.Mode = m - } -} - -// String returns the mode name. -func (m WindowMode) String() string { - switch m { - case Windowed: - return "windowed" - case Fullscreen: - return "fullscreen" - case Minimized: - return "minimized" - case Maximized: - return "maximized" - } - return "" -} - -// Orientation is the orientation of the app (Orientation.Option sets it). -// -// Supported platforms are Android and JS. -type Orientation uint8 - -const ( - // AnyOrientation allows the window to be freely orientated. - AnyOrientation Orientation = iota - // LandscapeOrientation constrains the window to landscape orientations. - LandscapeOrientation - // PortraitOrientation constrains the window to portrait orientations. - PortraitOrientation -) - -func (o Orientation) Option() Option { - return func(_ unit.Metric, cnf *Config) { - cnf.Orientation = o - } -} - -func (o Orientation) String() string { - switch o { - case AnyOrientation: - return "any" - case LandscapeOrientation: - return "landscape" - case PortraitOrientation: - return "portrait" - } - return "" -} - -// eventLoop implements the functionality required for drivers where -// window event loops must run on a separate thread. -type eventLoop struct { - win *callbacks - // wakeup is the callback to wake up the event loop. - wakeup func() - // driverFuncs is a channel of functions to run the next - // time the window loop waits for events. - driverFuncs chan func() - // invalidates is notified when an invalidate is requested by the client. - invalidates chan struct{} - // immediateInvalidates is an optimistic invalidates that doesn't require a wakeup. - immediateInvalidates chan struct{} - // events is where the platform backend delivers events bound for the - // user program. - events chan event.Event - frames chan *op.Ops - frameAck chan struct{} - // delivering avoids re-entrant event delivery. - delivering bool -} - -type frameEvent struct { - FrameEvent - - Sync bool -} - -type context interface { - API() gpu.API - RenderTarget() (gpu.RenderTarget, error) - Present() error - Refresh() error - Release() - Lock() error - Unlock() -} - -// driver is the interface for the platform implementation -// of a window. -type driver interface { - // Event blocks until an event is available and returns it. - Event() event.Event - // Invalidate requests a FrameEvent. - Invalidate() - // SetAnimating sets the animation flag. When the window is animating, - // FrameEvents are delivered as fast as the display can handle them. - SetAnimating(anim bool) - // ShowTextInput updates the virtual keyboard state. - ShowTextInput(show bool) - SetInputHint(mode key.InputHint) - NewContext() (context, error) - // ReadClipboard requests the clipboard content. - ReadClipboard() - // WriteClipboard requests a clipboard write. - WriteClipboard(mime string, s []byte) - // Configure the window. - Configure([]Option) - // SetCursor updates the current cursor to name. - SetCursor(cursor pointer.Cursor) - // Perform actions on the window. - Perform(system.Action) - // EditorStateChanged notifies the driver that the editor state changed. - EditorStateChanged(old, new editorState) - // Run a function on the window thread. - Run(f func()) - // Frame receives a frame. - Frame(frame *op.Ops) - // ProcessEvent processes an event. - ProcessEvent(e event.Event) -} - -type windowRendezvous struct { - in chan windowAndConfig - out chan windowAndConfig - windows chan struct{} -} - -type windowAndConfig struct { - window *callbacks - options []Option -} - -func newWindowRendezvous() *windowRendezvous { - wr := &windowRendezvous{ - in: make(chan windowAndConfig), - out: make(chan windowAndConfig), - windows: make(chan struct{}), - } - go func() { - in := wr.in - var window windowAndConfig - var out chan windowAndConfig - for { - select { - case w := <-in: - window = w - out = wr.out - case out <- window: - } - } - }() - return wr -} - -func newEventLoop(w *callbacks, wakeup func()) *eventLoop { - return &eventLoop{ - win: w, - wakeup: wakeup, - events: make(chan event.Event), - invalidates: make(chan struct{}, 1), - immediateInvalidates: make(chan struct{}), - frames: make(chan *op.Ops), - frameAck: make(chan struct{}), - driverFuncs: make(chan func(), 1), - } -} - -// Frame receives a frame and waits for its processing. It is called by -// the client goroutine. -func (e *eventLoop) Frame(frame *op.Ops) { - e.frames <- frame - <-e.frameAck -} - -// Event returns the next available event. It is called by the client -// goroutine. -func (e *eventLoop) Event() event.Event { - for { - evt := <-e.events - // Receiving a flushEvent indicates to the platform backend that - // all previous events have been processed by the user program. - if _, ok := evt.(flushEvent); ok { - continue - } - return evt - } -} - -// Invalidate requests invalidation of the window. It is called by the client -// goroutine. -func (e *eventLoop) Invalidate() { - select { - case e.immediateInvalidates <- struct{}{}: - // The event loop was waiting, no need for a wakeup. - case e.invalidates <- struct{}{}: - // The event loop is sleeping, wake it up. - e.wakeup() - default: - // A redraw is pending. - } -} - -// Run f in the window loop thread. It is called by the client goroutine. -func (e *eventLoop) Run(f func()) { - e.driverFuncs <- f - e.wakeup() -} - -// FlushEvents delivers pending events to the client. -func (e *eventLoop) FlushEvents() { - if e.delivering { - return - } - e.delivering = true - defer func() { e.delivering = false }() - for { - evt, ok := e.win.nextEvent() - if !ok { - break - } - e.deliverEvent(evt) - } -} - -func (e *eventLoop) deliverEvent(evt event.Event) { - var frames <-chan *op.Ops - for { - select { - case f := <-e.driverFuncs: - f() - case frame := <-frames: - // The client called FrameEvent.Frame. - frames = nil - e.win.ProcessFrame(frame, e.frameAck) - case e.events <- evt: - switch evt.(type) { - case flushEvent, DestroyEvent: - // DestroyEvents are not flushed. - return - case FrameEvent: - frames = e.frames - } - evt = theFlushEvent - case <-e.invalidates: - e.win.Invalidate() - case <-e.immediateInvalidates: - e.win.Invalidate() - } - } -} - -func (e *eventLoop) Wakeup() { - for { - select { - case f := <-e.driverFuncs: - f() - case <-e.invalidates: - e.win.Invalidate() - case <-e.immediateInvalidates: - e.win.Invalidate() - default: - return - } - } -} - -func walkActions(actions system.Action, do func(system.Action)) { - for a := system.Action(1); actions != 0; a <<= 1 { - if actions&a != 0 { - actions &^= a - do(a) - } - } -} - -func (wakeupEvent) ImplementsEvent() {} -func (ConfigEvent) ImplementsEvent() {} diff --git a/gio/app/os_android.go b/gio/app/os_android.go deleted file mode 100644 index edb5cac..0000000 --- a/gio/app/os_android.go +++ /dev/null @@ -1,1500 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -/* -#cgo CFLAGS: -Werror -#cgo LDFLAGS: -landroid - -#include -#include -#include -#include -#include - -static jint jni_GetEnv(JavaVM *vm, JNIEnv **env, jint version) { - return (*vm)->GetEnv(vm, (void **)env, version); -} - -static jint jni_GetJavaVM(JNIEnv *env, JavaVM **jvm) { - return (*env)->GetJavaVM(env, jvm); -} - -static jint jni_AttachCurrentThread(JavaVM *vm, JNIEnv **p_env, void *thr_args) { - return (*vm)->AttachCurrentThread(vm, p_env, thr_args); -} - -static jint jni_DetachCurrentThread(JavaVM *vm) { - return (*vm)->DetachCurrentThread(vm); -} - -static jobject jni_NewGlobalRef(JNIEnv *env, jobject obj) { - return (*env)->NewGlobalRef(env, obj); -} - -static void jni_DeleteGlobalRef(JNIEnv *env, jobject obj) { - (*env)->DeleteGlobalRef(env, obj); -} - -static jclass jni_GetObjectClass(JNIEnv *env, jobject obj) { - return (*env)->GetObjectClass(env, obj); -} - -static jmethodID jni_GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { - return (*env)->GetMethodID(env, clazz, name, sig); -} - -static jmethodID jni_GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig) { - return (*env)->GetStaticMethodID(env, clazz, name, sig); -} - -static jfloat jni_CallFloatMethod(JNIEnv *env, jobject obj, jmethodID methodID) { - return (*env)->CallFloatMethod(env, obj, methodID); -} - -static jint jni_CallIntMethod(JNIEnv *env, jobject obj, jmethodID methodID) { - return (*env)->CallIntMethod(env, obj, methodID); -} - -static void jni_CallStaticVoidMethodA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args) { - (*env)->CallStaticVoidMethodA(env, cls, methodID, args); -} - -static void jni_CallVoidMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args) { - (*env)->CallVoidMethodA(env, obj, methodID, args); -} - -static jboolean jni_CallBooleanMethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args) { - return (*env)->CallBooleanMethodA(env, obj, methodID, args); -} - -static jbyte *jni_GetByteArrayElements(JNIEnv *env, jbyteArray arr) { - return (*env)->GetByteArrayElements(env, arr, NULL); -} - -static void jni_ReleaseByteArrayElements(JNIEnv *env, jbyteArray arr, jbyte *bytes) { - (*env)->ReleaseByteArrayElements(env, arr, bytes, JNI_ABORT); -} - -static jsize jni_GetArrayLength(JNIEnv *env, jbyteArray arr) { - return (*env)->GetArrayLength(env, arr); -} - -static jstring jni_NewString(JNIEnv *env, const jchar *unicodeChars, jsize len) { - return (*env)->NewString(env, unicodeChars, len); -} - -static jsize jni_GetStringLength(JNIEnv *env, jstring str) { - return (*env)->GetStringLength(env, str); -} - -static const jchar *jni_GetStringChars(JNIEnv *env, jstring str) { - return (*env)->GetStringChars(env, str, NULL); -} - -static jthrowable jni_ExceptionOccurred(JNIEnv *env) { - return (*env)->ExceptionOccurred(env); -} - -static void jni_ExceptionClear(JNIEnv *env) { - (*env)->ExceptionClear(env); -} - -static jobject jni_CallObjectMethodA(JNIEnv *env, jobject obj, jmethodID method, jvalue *args) { - return (*env)->CallObjectMethodA(env, obj, method, args); -} - -static jobject jni_CallStaticObjectMethodA(JNIEnv *env, jclass cls, jmethodID method, jvalue *args) { - return (*env)->CallStaticObjectMethodA(env, cls, method, args); -} - -static jclass jni_FindClass(JNIEnv *env, char *name) { - return (*env)->FindClass(env, name); -} - -static jobject jni_NewObjectA(JNIEnv *env, jclass cls, jmethodID cons, jvalue *args) { - return (*env)->NewObjectA(env, cls, cons, args); -} -*/ -import "C" - -import ( - "errors" - "fmt" - "image" - "image/color" - "io" - "math" - "os" - "path/filepath" - "runtime" - "runtime/cgo" - "runtime/debug" - "strings" - "sync" - "time" - "unicode/utf16" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/op" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -type window struct { - callbacks *callbacks - loop *eventLoop - - view C.jobject - handle cgo.Handle - - dpi int - fontScale float32 - insets pixelInsets - - visible bool - started bool - animating bool - - win *C.ANativeWindow - config Config - inputHint key.InputHint - - semantic struct { - hoverID input.SemanticID - rootID input.SemanticID - focusID input.SemanticID - diffs []input.SemanticID - } -} - -// gioView hold cached JNI methods for GioView. -var gioView struct { - once sync.Once - getDensity C.jmethodID - getFontScale C.jmethodID - showTextInput C.jmethodID - hideTextInput C.jmethodID - setInputHint C.jmethodID - postFrameCallback C.jmethodID - invalidate C.jmethodID // requests draw, called from UI thread - setCursor C.jmethodID - setOrientation C.jmethodID - setNavigationColor C.jmethodID - setStatusColor C.jmethodID - setFullscreen C.jmethodID - unregister C.jmethodID - sendA11yEvent C.jmethodID - sendA11yChange C.jmethodID - isA11yActive C.jmethodID - restartInput C.jmethodID - updateSelection C.jmethodID - updateCaret C.jmethodID -} - -type pixelInsets struct { - top, bottom, left, right int -} - -// AndroidViewEvent is sent whenever the Window's underlying Android view -// changes. -type AndroidViewEvent struct { - // View is a JNI global reference to the android.view.View - // instance backing the Window. The reference is valid until - // the next ViewEvent is received. - // A zero View means that there is currently no view attached. - View uintptr -} - -type jvalue uint64 // The largest JNI type fits in 64 bits. - -var dataDirChan = make(chan string, 1) - -var android struct { - // mu protects all fields of this structure. However, once a - // non-nil jvm is returned from javaVM, all the other fields may - // be accessed unlocked. - mu sync.Mutex - jvm *C.JavaVM - - // appCtx is the global Android App context. - appCtx C.jobject - // gioCls is the class of the Gio class. - gioCls C.jclass - - mwriteClipboard C.jmethodID - mreadClipboard C.jmethodID - mwakeupMainThread C.jmethodID - - // android.view.accessibility.AccessibilityNodeInfo class. - accessibilityNodeInfo struct { - cls C.jclass - // addChild(View, int) - addChild C.jmethodID - // setBoundsInScreen(Rect) - setBoundsInScreen C.jmethodID - // setText(CharSequence) - setText C.jmethodID - // setContentDescription(CharSequence) - setContentDescription C.jmethodID - // setParent(View, int) - setParent C.jmethodID - // addAction(int) - addAction C.jmethodID - // setClassName(CharSequence) - setClassName C.jmethodID - // setCheckable(boolean) - setCheckable C.jmethodID - // setSelected(boolean) - setSelected C.jmethodID - // setChecked(boolean) - setChecked C.jmethodID - // setEnabled(boolean) - setEnabled C.jmethodID - // setAccessibilityFocused(boolean) - setAccessibilityFocused C.jmethodID - } - - // android.graphics.Rect class. - rect struct { - cls C.jclass - // (int, int, int, int) constructor. - cons C.jmethodID - } - - strings struct { - // "android.view.View" - androidViewView C.jstring - // "android.widget.Button" - androidWidgetButton C.jstring - // "android.widget.CheckBox" - androidWidgetCheckBox C.jstring - // "android.widget.EditText" - androidWidgetEditText C.jstring - // "android.widget.RadioButton" - androidWidgetRadioButton C.jstring - // "android.widget.Switch" - androidWidgetSwitch C.jstring - } -} - -var windows = make(map[*callbacks]*window) - -var mainWindow = newWindowRendezvous() - -var mainFuncs = make(chan func(env *C.JNIEnv), 1) - -var ( - dataDirOnce sync.Once - dataPath string -) - -var ( - newAndroidVulkanContext func(w *window) (context, error) - newAndroidGLESContext func(w *window) (context, error) -) - -// AccessibilityNodeProvider.HOST_VIEW_ID. -const HOST_VIEW_ID = -1 - -const ( - // AccessibilityEvent constants. - TYPE_VIEW_HOVER_ENTER = 128 - TYPE_VIEW_HOVER_EXIT = 256 -) - -const ( - // AccessibilityNodeInfo constants. - ACTION_ACCESSIBILITY_FOCUS = 64 - ACTION_CLEAR_ACCESSIBILITY_FOCUS = 128 - ACTION_CLICK = 16 -) - -func (w *window) NewContext() (context, error) { - funcs := []func(w *window) (context, error){newAndroidGLESContext, newAndroidVulkanContext} - var firstErr error - for _, f := range funcs { - if f == nil { - continue - } - c, err := f(w) - if err != nil { - if firstErr == nil { - firstErr = err - } - continue - } - return c, nil - } - if firstErr != nil { - return nil, firstErr - } - return nil, errors.New("x11: no available GPU backends") -} - -func dataDir() (string, error) { - dataDirOnce.Do(func() { - dataPath = <-dataDirChan - }) - return dataPath, nil -} - -func getMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID { - m := C.CString(method) - defer C.free(unsafe.Pointer(m)) - s := C.CString(sig) - defer C.free(unsafe.Pointer(s)) - jm := C.jni_GetMethodID(env, class, m, s) - if err := exception(env); err != nil { - panic(err) - } - return jm -} - -func getStaticMethodID(env *C.JNIEnv, class C.jclass, method, sig string) C.jmethodID { - m := C.CString(method) - defer C.free(unsafe.Pointer(m)) - s := C.CString(sig) - defer C.free(unsafe.Pointer(s)) - jm := C.jni_GetStaticMethodID(env, class, m, s) - if err := exception(env); err != nil { - panic(err) - } - return jm -} - -//export Java_org_gioui_Gio_runGoMain -func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, jdataDir C.jbyteArray, context C.jobject) { - initJVM(env, class, context) - dirBytes := C.jni_GetByteArrayElements(env, jdataDir) - if dirBytes == nil { - panic("runGoMain: GetByteArrayElements failed") - } - n := C.jni_GetArrayLength(env, jdataDir) - dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n) - - // Set XDG_CACHE_HOME to make os.UserCacheDir work. - if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists { - cachePath := filepath.Join(dataDir, "cache") - os.Setenv("XDG_CACHE_HOME", cachePath) - } - // Set XDG_CONFIG_HOME to make os.UserConfigDir work. - if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists { - cfgPath := filepath.Join(dataDir, "config") - os.Setenv("XDG_CONFIG_HOME", cfgPath) - } - // Set HOME to make os.UserHomeDir work. - if _, exists := os.LookupEnv("HOME"); !exists { - os.Setenv("HOME", dataDir) - } - - dataDirChan <- dataDir - C.jni_ReleaseByteArrayElements(env, jdataDir, dirBytes) - - runMain() -} - -func initJVM(env *C.JNIEnv, gio C.jclass, ctx C.jobject) { - android.mu.Lock() - defer android.mu.Unlock() - if res := C.jni_GetJavaVM(env, &android.jvm); res != 0 { - panic("gio: GetJavaVM failed") - } - android.appCtx = C.jni_NewGlobalRef(env, ctx) - android.gioCls = C.jclass(C.jni_NewGlobalRef(env, C.jobject(gio))) - - cls := findClass(env, "android/view/accessibility/AccessibilityNodeInfo") - android.accessibilityNodeInfo.cls = C.jclass(C.jni_NewGlobalRef(env, C.jobject(cls))) - android.accessibilityNodeInfo.addChild = getMethodID(env, cls, "addChild", "(Landroid/view/View;I)V") - android.accessibilityNodeInfo.setBoundsInScreen = getMethodID(env, cls, "setBoundsInScreen", "(Landroid/graphics/Rect;)V") - android.accessibilityNodeInfo.setText = getMethodID(env, cls, "setText", "(Ljava/lang/CharSequence;)V") - android.accessibilityNodeInfo.setContentDescription = getMethodID(env, cls, "setContentDescription", "(Ljava/lang/CharSequence;)V") - android.accessibilityNodeInfo.setParent = getMethodID(env, cls, "setParent", "(Landroid/view/View;I)V") - android.accessibilityNodeInfo.addAction = getMethodID(env, cls, "addAction", "(I)V") - android.accessibilityNodeInfo.setClassName = getMethodID(env, cls, "setClassName", "(Ljava/lang/CharSequence;)V") - android.accessibilityNodeInfo.setCheckable = getMethodID(env, cls, "setCheckable", "(Z)V") - android.accessibilityNodeInfo.setSelected = getMethodID(env, cls, "setSelected", "(Z)V") - android.accessibilityNodeInfo.setChecked = getMethodID(env, cls, "setChecked", "(Z)V") - android.accessibilityNodeInfo.setEnabled = getMethodID(env, cls, "setEnabled", "(Z)V") - android.accessibilityNodeInfo.setAccessibilityFocused = getMethodID(env, cls, "setAccessibilityFocused", "(Z)V") - - cls = findClass(env, "android/graphics/Rect") - android.rect.cls = C.jclass(C.jni_NewGlobalRef(env, C.jobject(cls))) - android.rect.cons = getMethodID(env, cls, "", "(IIII)V") - android.mwriteClipboard = getStaticMethodID(env, gio, "writeClipboard", "(Landroid/content/Context;Ljava/lang/String;)V") - android.mreadClipboard = getStaticMethodID(env, gio, "readClipboard", "(Landroid/content/Context;)Ljava/lang/String;") - android.mwakeupMainThread = getStaticMethodID(env, gio, "wakeupMainThread", "()V") - - intern := func(s string) C.jstring { - ref := C.jni_NewGlobalRef(env, C.jobject(javaString(env, s))) - return C.jstring(ref) - } - android.strings.androidViewView = intern("android.view.View") - android.strings.androidWidgetButton = intern("android.widget.Button") - android.strings.androidWidgetCheckBox = intern("android.widget.CheckBox") - android.strings.androidWidgetEditText = intern("android.widget.EditText") - android.strings.androidWidgetRadioButton = intern("android.widget.RadioButton") - android.strings.androidWidgetSwitch = intern("android.widget.Switch") -} - -// JavaVM returns the global JNI JavaVM. -func JavaVM() uintptr { - jvm := javaVM() - return uintptr(unsafe.Pointer(jvm)) -} - -func javaVM() *C.JavaVM { - android.mu.Lock() - defer android.mu.Unlock() - return android.jvm -} - -// AppContext returns the global Application context as a JNI jobject. -func AppContext() uintptr { - android.mu.Lock() - defer android.mu.Unlock() - return uintptr(android.appCtx) -} - -//export Java_org_gioui_GioView_onCreateView -func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.jobject) C.jlong { - gioView.once.Do(func() { - m := &gioView - m.getDensity = getMethodID(env, class, "getDensity", "()I") - m.getFontScale = getMethodID(env, class, "getFontScale", "()F") - m.showTextInput = getMethodID(env, class, "showTextInput", "()V") - m.hideTextInput = getMethodID(env, class, "hideTextInput", "()V") - m.setInputHint = getMethodID(env, class, "setInputHint", "(I)V") - m.postFrameCallback = getMethodID(env, class, "postFrameCallback", "()V") - m.invalidate = getMethodID(env, class, "invalidate", "()V") - m.setCursor = getMethodID(env, class, "setCursor", "(I)V") - m.setOrientation = getMethodID(env, class, "setOrientation", "(II)V") - m.setNavigationColor = getMethodID(env, class, "setNavigationColor", "(II)V") - m.setStatusColor = getMethodID(env, class, "setStatusColor", "(II)V") - m.setFullscreen = getMethodID(env, class, "setFullscreen", "(Z)V") - m.unregister = getMethodID(env, class, "unregister", "()V") - m.sendA11yEvent = getMethodID(env, class, "sendA11yEvent", "(II)V") - m.sendA11yChange = getMethodID(env, class, "sendA11yChange", "(I)V") - m.isA11yActive = getMethodID(env, class, "isA11yActive", "()Z") - m.restartInput = getMethodID(env, class, "restartInput", "()V") - m.updateSelection = getMethodID(env, class, "updateSelection", "()V") - m.updateCaret = getMethodID(env, class, "updateCaret", "(FFFFFFFFFF)V") - }) - view = C.jni_NewGlobalRef(env, view) - wopts := <-mainWindow.out - var cnf Config - w, ok := windows[wopts.window] - if !ok { - w = &window{ - callbacks: wopts.window, - } - w.loop = newEventLoop(w.callbacks, w.wakeup) - w.callbacks.SetDriver(w) - cnf.apply(unit.Metric{}, wopts.options) - windows[wopts.window] = w - } else { - cnf = w.config - } - mainWindow.windows <- struct{}{} - if w.view != 0 { - w.detach(env) - } - w.view = view - w.visible = false - w.handle = cgo.NewHandle(w) - w.loadConfig(env, class) - w.setConfig(env, cnf) - w.SetInputHint(w.inputHint) - w.processEvent(AndroidViewEvent{View: uintptr(view)}) - return C.jlong(w.handle) -} - -//export Java_org_gioui_GioView_onDestroyView -func Java_org_gioui_GioView_onDestroyView(env *C.JNIEnv, class C.jclass, handle C.jlong) { - w := cgo.Handle(handle).Value().(*window) - w.detach(env) -} - -//export Java_org_gioui_GioView_onStopView -func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, handle C.jlong) { - w := cgo.Handle(handle).Value().(*window) - w.started = false - w.visible = false -} - -//export Java_org_gioui_GioView_onStartView -func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, handle C.jlong) { - w := cgo.Handle(handle).Value().(*window) - w.started = true - if w.win != nil { - w.setVisible(env) - } -} - -//export Java_org_gioui_GioView_onSurfaceDestroyed -func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) { - w := cgo.Handle(handle).Value().(*window) - w.win = nil - w.visible = false -} - -//export Java_org_gioui_GioView_onSurfaceChanged -func Java_org_gioui_GioView_onSurfaceChanged(env *C.JNIEnv, class C.jclass, handle C.jlong, surf C.jobject) { - w := cgo.Handle(handle).Value().(*window) - w.win = C.ANativeWindow_fromSurface(env, surf) - if w.started { - w.setVisible(env) - } -} - -//export Java_org_gioui_GioView_onLowMemory -func Java_org_gioui_GioView_onLowMemory(env *C.JNIEnv, class C.jclass) { - runtime.GC() - debug.FreeOSMemory() -} - -//export Java_org_gioui_GioView_onConfigurationChanged -func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) { - w := cgo.Handle(view).Value().(*window) - w.loadConfig(env, class) - w.draw(env, true) -} - -//export Java_org_gioui_GioView_onFrameCallback -func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view C.jlong) { - w, exist := cgo.Handle(view).Value().(*window) - if !exist { - return - } - w.draw(env, false) -} - -//export Java_org_gioui_GioView_onBack -func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean { - w := cgo.Handle(view).Value().(*window) - if w.processEvent(key.Event{Name: key.NameBack}) { - return C.JNI_TRUE - } - return C.JNI_FALSE -} - -//export Java_org_gioui_GioView_onFocusChange -func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) { - w := cgo.Handle(view).Value().(*window) - w.config.Focused = focus == C.JNI_TRUE - w.processEvent(ConfigEvent{Config: w.config}) -} - -//export Java_org_gioui_GioView_onWindowInsets -func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C.jlong, top, right, bottom, left C.jint) { - w := cgo.Handle(view).Value().(*window) - w.insets = pixelInsets{ - top: int(top), - bottom: int(bottom), - left: int(left), - right: int(right), - } - w.draw(env, true) -} - -//export Java_org_gioui_GioView_initializeAccessibilityNodeInfo -func Java_org_gioui_GioView_initializeAccessibilityNodeInfo(env *C.JNIEnv, class C.jclass, view C.jlong, virtID, screenX, screenY C.jint, info C.jobject) C.jobject { - w := cgo.Handle(view).Value().(*window) - semID := w.semIDFor(virtID) - sem, found := w.callbacks.LookupSemantic(semID) - if found { - off := image.Pt(int(screenX), int(screenY)) - if err := w.initAccessibilityNodeInfo(env, sem, off, info); err != nil { - panic(err) - } - } - return info -} - -//export Java_org_gioui_GioView_onTouchExploration -func Java_org_gioui_GioView_onTouchExploration(env *C.JNIEnv, class C.jclass, view C.jlong, x, y C.jfloat) { - w := cgo.Handle(view).Value().(*window) - semID, _ := w.callbacks.SemanticAt(f32.Pt(float32(x), float32(y))) - if w.semantic.hoverID == semID { - return - } - // Android expects ENTER before EXIT. - if semID != 0 { - callVoidMethod(env, w.view, gioView.sendA11yEvent, TYPE_VIEW_HOVER_ENTER, jvalue(w.virtualIDFor(semID))) - } - if prevID := w.semantic.hoverID; prevID != 0 { - callVoidMethod(env, w.view, gioView.sendA11yEvent, TYPE_VIEW_HOVER_EXIT, jvalue(w.virtualIDFor(prevID))) - } - w.semantic.hoverID = semID -} - -//export Java_org_gioui_GioView_onExitTouchExploration -func Java_org_gioui_GioView_onExitTouchExploration(env *C.JNIEnv, class C.jclass, view C.jlong) { - w := cgo.Handle(view).Value().(*window) - if w.semantic.hoverID != 0 { - callVoidMethod(env, w.view, gioView.sendA11yEvent, TYPE_VIEW_HOVER_EXIT, jvalue(w.virtualIDFor(w.semantic.hoverID))) - w.semantic.hoverID = 0 - } -} - -//export Java_org_gioui_GioView_onA11yFocus -func Java_org_gioui_GioView_onA11yFocus(env *C.JNIEnv, class C.jclass, view C.jlong, virtID C.jint) { - w := cgo.Handle(view).Value().(*window) - if semID := w.semIDFor(virtID); semID != w.semantic.focusID { - w.semantic.focusID = semID - // Android needs invalidate to refresh the TalkBack focus indicator. - callVoidMethod(env, w.view, gioView.invalidate) - } -} - -//export Java_org_gioui_GioView_onClearA11yFocus -func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view C.jlong, virtID C.jint) { - w := cgo.Handle(view).Value().(*window) - if w.semantic.focusID == w.semIDFor(virtID) { - w.semantic.focusID = 0 - } -} - -func (w *window) ProcessEvent(e event.Event) { - w.processEvent(e) -} - -func (w *window) processEvent(e event.Event) bool { - if !w.callbacks.ProcessEvent(e) { - return false - } - w.loop.FlushEvents() - return true -} - -func (w *window) Event() event.Event { - return w.loop.Event() -} - -func (w *window) Invalidate() { - w.loop.Invalidate() -} - -func (w *window) Run(f func()) { - w.loop.Run(f) -} - -func (w *window) Frame(frame *op.Ops) { - w.loop.Frame(frame) -} - -func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem input.SemanticNode, off image.Point, info C.jobject) error { - for _, ch := range sem.Children { - err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID))) - if err != nil { - return err - } - } - if sem.ParentID != 0 { - if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setParent, jvalue(w.view), jvalue(w.virtualIDFor(sem.ParentID))); err != nil { - return err - } - b := sem.Desc.Bounds.Add(off) - rect, err := newObject(env, android.rect.cls, android.rect.cons, - jvalue(b.Min.X), - jvalue(b.Min.Y), - jvalue(b.Max.X), - jvalue(b.Max.Y), - ) - if err != nil { - return err - } - if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setBoundsInScreen, jvalue(rect)); err != nil { - return err - } - } - d := sem.Desc - if l := d.Label; l != "" { - jlbl := javaString(env, l) - if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setText, jvalue(jlbl)); err != nil { - return err - } - } - if d.Description != "" { - jd := javaString(env, d.Description) - if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setContentDescription, jvalue(jd)); err != nil { - return err - } - } - addAction := func(act C.jint) { - if err := callVoidMethod(env, info, android.accessibilityNodeInfo.addAction, jvalue(act)); err != nil { - panic(err) - } - } - if d.Gestures&input.ClickGesture != 0 { - addAction(ACTION_CLICK) - } - clsName := android.strings.androidViewView - selectMethod := android.accessibilityNodeInfo.setChecked - checkable := false - switch d.Class { - case semantic.Button: - clsName = android.strings.androidWidgetButton - case semantic.CheckBox: - checkable = true - clsName = android.strings.androidWidgetCheckBox - case semantic.Editor: - clsName = android.strings.androidWidgetEditText - case semantic.RadioButton: - selectMethod = android.accessibilityNodeInfo.setSelected - clsName = android.strings.androidWidgetRadioButton - case semantic.Switch: - checkable = true - clsName = android.strings.androidWidgetSwitch - } - if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setClassName, jvalue(clsName)); err != nil { - panic(err) - } - if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setCheckable, jvalue(javaBool(checkable))); err != nil { - panic(err) - } - if err := callVoidMethod(env, info, selectMethod, jvalue(javaBool(d.Selected))); err != nil { - panic(err) - } - if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setEnabled, jvalue(javaBool(!d.Disabled))); err != nil { - panic(err) - } - isFocus := w.semantic.focusID == sem.ID - if err := callVoidMethod(env, info, android.accessibilityNodeInfo.setAccessibilityFocused, jvalue(javaBool(isFocus))); err != nil { - panic(err) - } - if isFocus { - addAction(ACTION_CLEAR_ACCESSIBILITY_FOCUS) - } else { - addAction(ACTION_ACCESSIBILITY_FOCUS) - } - return nil -} - -func (w *window) virtualIDFor(id input.SemanticID) C.jint { - if id == w.semantic.rootID { - return HOST_VIEW_ID - } - return C.jint(id) -} - -func (w *window) semIDFor(virtID C.jint) input.SemanticID { - if virtID == HOST_VIEW_ID { - return w.semantic.rootID - } - return input.SemanticID(virtID) -} - -func (w *window) detach(env *C.JNIEnv) { - callVoidMethod(env, w.view, gioView.unregister) - w.processEvent(AndroidViewEvent{}) - w.handle.Delete() - C.jni_DeleteGlobalRef(env, w.view) - w.view = 0 -} - -func (w *window) setVisible(env *C.JNIEnv) { - width, height := C.ANativeWindow_getWidth(w.win), C.ANativeWindow_getHeight(w.win) - if width == 0 || height == 0 { - return - } - w.visible = true - w.draw(env, true) -} - -func (w *window) setVisual(visID int) error { - if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 { - return errors.New("ANativeWindow_setBuffersGeometry failed") - } - return nil -} - -func (w *window) nativeWindow() (*C.ANativeWindow, int, int) { - width, height := C.ANativeWindow_getWidth(w.win), C.ANativeWindow_getHeight(w.win) - return w.win, int(width), int(height) -} - -func (w *window) loadConfig(env *C.JNIEnv, class C.jclass) { - dpi := int(C.jni_CallIntMethod(env, w.view, gioView.getDensity)) - w.fontScale = float32(C.jni_CallFloatMethod(env, w.view, gioView.getFontScale)) - switch dpi { - case C.ACONFIGURATION_DENSITY_NONE, - C.ACONFIGURATION_DENSITY_DEFAULT, - C.ACONFIGURATION_DENSITY_ANY: - // Assume standard density. - w.dpi = C.ACONFIGURATION_DENSITY_MEDIUM - default: - w.dpi = int(dpi) - } -} - -func (w *window) SetAnimating(anim bool) { - w.animating = anim - if anim { - runInJVM(javaVM(), func(env *C.JNIEnv) { - callVoidMethod(env, w.view, gioView.postFrameCallback) - }) - } -} - -func (w *window) draw(env *C.JNIEnv, sync bool) { - if !w.visible { - return - } - size := image.Pt(int(C.ANativeWindow_getWidth(w.win)), int(C.ANativeWindow_getHeight(w.win))) - if size != w.config.Size { - w.config.Size = size - w.processEvent(ConfigEvent{Config: w.config}) - } - if size.X == 0 || size.Y == 0 { - return - } - const inchPrDp = 1.0 / 160 - ppdp := float32(w.dpi) * inchPrDp - dppp := unit.Dp(1.0 / ppdp) - insets := Insets{ - Top: unit.Dp(w.insets.top) * dppp, - Bottom: unit.Dp(w.insets.bottom) * dppp, - Left: unit.Dp(w.insets.left) * dppp, - Right: unit.Dp(w.insets.right) * dppp, - } - w.processEvent(frameEvent{ - FrameEvent: FrameEvent{ - Now: time.Now(), - Size: w.config.Size, - Insets: insets, - Metric: unit.Metric{ - PxPerDp: ppdp, - PxPerSp: w.fontScale * ppdp, - }, - }, - Sync: sync, - }) - if w.animating { - callVoidMethod(env, w.view, gioView.postFrameCallback) - } - a11yActive, err := callBooleanMethod(env, w.view, gioView.isA11yActive) - if err != nil { - panic(err) - } - if a11yActive { - if newR, oldR := w.callbacks.SemanticRoot(), w.semantic.rootID; newR != oldR { - // Remap focus and hover. - if oldR == w.semantic.hoverID { - w.semantic.hoverID = newR - } - if oldR == w.semantic.focusID { - w.semantic.focusID = newR - } - w.semantic.rootID = newR - callVoidMethod(env, w.view, gioView.sendA11yChange, jvalue(w.virtualIDFor(newR))) - } - w.semantic.diffs = w.callbacks.AppendSemanticDiffs(w.semantic.diffs[:0]) - for _, id := range w.semantic.diffs { - callVoidMethod(env, w.view, gioView.sendA11yChange, jvalue(w.virtualIDFor(id))) - } - } -} - -func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) { - if jvm == nil { - panic("nil JVM") - } - runtime.LockOSThread() - defer runtime.UnlockOSThread() - var env *C.JNIEnv - if res := C.jni_GetEnv(jvm, &env, C.JNI_VERSION_1_6); res != C.JNI_OK { - if res != C.JNI_EDETACHED { - panic(fmt.Errorf("JNI GetEnv failed with error %d", res)) - } - if C.jni_AttachCurrentThread(jvm, &env, nil) != C.JNI_OK { - panic(errors.New("runInJVM: AttachCurrentThread failed")) - } - defer C.jni_DetachCurrentThread(jvm) - } - - f(env) -} - -func convertKeyCode(code C.jint) (key.Name, bool) { - var n key.Name - switch code { - case C.AKEYCODE_FORWARD_DEL: - n = key.NameDeleteForward - case C.AKEYCODE_DEL: - n = key.NameDeleteBackward - case C.AKEYCODE_NUMPAD_ENTER: - n = key.NameEnter - case C.AKEYCODE_ENTER: - n = key.NameReturn - case C.AKEYCODE_CTRL_LEFT, C.AKEYCODE_CTRL_RIGHT: - n = key.NameCtrl - case C.AKEYCODE_SHIFT_LEFT, C.AKEYCODE_SHIFT_RIGHT: - n = key.NameShift - case C.AKEYCODE_ALT_LEFT, C.AKEYCODE_ALT_RIGHT: - n = key.NameAlt - case C.AKEYCODE_META_LEFT, C.AKEYCODE_META_RIGHT: - n = key.NameSuper - case C.AKEYCODE_DPAD_UP: - n = key.NameUpArrow - case C.AKEYCODE_DPAD_DOWN: - n = key.NameDownArrow - case C.AKEYCODE_DPAD_LEFT: - n = key.NameLeftArrow - case C.AKEYCODE_DPAD_RIGHT: - n = key.NameRightArrow - default: - return "", false - } - return n, true -} - -//export Java_org_gioui_GioView_onKeyEvent -func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, keyCode, r C.jint, pressed C.jboolean, t C.jlong) { - w := cgo.Handle(handle).Value().(*window) - if pressed == C.JNI_TRUE && keyCode == C.AKEYCODE_DPAD_CENTER { - w.callbacks.ClickFocus() - return - } - if n, ok := convertKeyCode(keyCode); ok { - state := key.Release - if pressed == C.JNI_TRUE { - state = key.Press - } - w.processEvent(key.Event{Name: n, State: state}) - } - if pressed == C.JNI_TRUE && r != 0 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224). - w.callbacks.EditorInsert(string(rune(r))) - } -} - -//export Java_org_gioui_GioView_onTouchEvent -func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, action, pointerID, tool C.jint, x, y, scrollX, scrollY C.jfloat, jbtns C.jint, t C.jlong) { - w := cgo.Handle(handle).Value().(*window) - var kind pointer.Kind - switch action { - case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN: - kind = pointer.Press - case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP: - kind = pointer.Release - case C.AMOTION_EVENT_ACTION_CANCEL: - kind = pointer.Cancel - case C.AMOTION_EVENT_ACTION_MOVE: - kind = pointer.Move - case C.AMOTION_EVENT_ACTION_SCROLL: - kind = pointer.Scroll - default: - return - } - var src pointer.Source - var btns pointer.Buttons - if jbtns&C.AMOTION_EVENT_BUTTON_PRIMARY != 0 { - btns |= pointer.ButtonPrimary - } - if jbtns&C.AMOTION_EVENT_BUTTON_SECONDARY != 0 { - btns |= pointer.ButtonSecondary - } - if jbtns&C.AMOTION_EVENT_BUTTON_TERTIARY != 0 { - btns |= pointer.ButtonTertiary - } - switch tool { - case C.AMOTION_EVENT_TOOL_TYPE_FINGER: - src = pointer.Touch - case C.AMOTION_EVENT_TOOL_TYPE_STYLUS: - src = pointer.Touch - case C.AMOTION_EVENT_TOOL_TYPE_MOUSE: - src = pointer.Mouse - case C.AMOTION_EVENT_TOOL_TYPE_UNKNOWN: - // For example, triggered via 'adb shell input tap'. - // Instead of discarding it, treat it as a touch event. - src = pointer.Touch - default: - return - } - w.processEvent(pointer.Event{ - Kind: kind, - Source: src, - Buttons: btns, - PointerID: pointer.ID(pointerID), - Time: time.Duration(t) * time.Millisecond, - Position: f32.Point{X: float32(x), Y: float32(y)}, - Scroll: f32.Pt(float32(scrollX), float32(scrollY)), - }) -} - -//export Java_org_gioui_GioView_imeSelectionStart -func Java_org_gioui_GioView_imeSelectionStart(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint { - w := cgo.Handle(handle).Value().(*window) - sel := w.callbacks.EditorState().Selection - start := sel.Start - if sel.End < sel.Start { - start = sel.End - } - return C.jint(start) -} - -//export Java_org_gioui_GioView_imeSelectionEnd -func Java_org_gioui_GioView_imeSelectionEnd(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint { - w := cgo.Handle(handle).Value().(*window) - sel := w.callbacks.EditorState().Selection - end := sel.End - if sel.End < sel.Start { - end = sel.Start - } - return C.jint(end) -} - -//export Java_org_gioui_GioView_imeComposingStart -func Java_org_gioui_GioView_imeComposingStart(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint { - w := cgo.Handle(handle).Value().(*window) - comp := w.callbacks.EditorState().compose - start := comp.Start - if e := comp.End; e < start { - start = e - } - return C.jint(start) -} - -//export Java_org_gioui_GioView_imeComposingEnd -func Java_org_gioui_GioView_imeComposingEnd(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint { - w := cgo.Handle(handle).Value().(*window) - comp := w.callbacks.EditorState().compose - end := comp.End - if s := comp.Start; s > end { - end = s - } - return C.jint(end) -} - -//export Java_org_gioui_GioView_imeSnippet -func Java_org_gioui_GioView_imeSnippet(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jstring { - w := cgo.Handle(handle).Value().(*window) - snip := w.callbacks.EditorState().Snippet.Text - return javaString(env, snip) -} - -//export Java_org_gioui_GioView_imeSnippetStart -func Java_org_gioui_GioView_imeSnippetStart(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint { - w := cgo.Handle(handle).Value().(*window) - return C.jint(w.callbacks.EditorState().Snippet.Start) -} - -//export Java_org_gioui_GioView_imeSetSnippet -func Java_org_gioui_GioView_imeSetSnippet(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) { - w := cgo.Handle(handle).Value().(*window) - if start < 0 { - start = 0 - } - if end < start { - end = start - } - r := key.Range{Start: int(start), End: int(end)} - w.callbacks.SetEditorSnippet(r) -} - -//export Java_org_gioui_GioView_imeSetSelection -func Java_org_gioui_GioView_imeSetSelection(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) { - w := cgo.Handle(handle).Value().(*window) - r := key.Range{Start: int(start), End: int(end)} - w.callbacks.SetEditorSelection(r) -} - -//export Java_org_gioui_GioView_imeSetComposingRegion -func Java_org_gioui_GioView_imeSetComposingRegion(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) { - w := cgo.Handle(handle).Value().(*window) - w.callbacks.SetComposingRegion(key.Range{ - Start: int(start), - End: int(end), - }) -} - -//export Java_org_gioui_GioView_imeReplace -func Java_org_gioui_GioView_imeReplace(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint, jtext C.jstring) { - w := cgo.Handle(handle).Value().(*window) - r := key.Range{Start: int(start), End: int(end)} - text := goString(env, jtext) - w.callbacks.EditorReplace(r, text) -} - -//export Java_org_gioui_GioView_imeToRunes -func Java_org_gioui_GioView_imeToRunes(env *C.JNIEnv, class C.jclass, handle C.jlong, chars C.jint) C.jint { - w := cgo.Handle(handle).Value().(*window) - state := w.callbacks.EditorState() - return C.jint(state.RunesIndex(int(chars))) -} - -//export Java_org_gioui_GioView_imeToUTF16 -func Java_org_gioui_GioView_imeToUTF16(env *C.JNIEnv, class C.jclass, handle C.jlong, runes C.jint) C.jint { - w := cgo.Handle(handle).Value().(*window) - state := w.callbacks.EditorState() - return C.jint(state.UTF16Index(int(runes))) -} - -func (w *window) EditorStateChanged(old, new editorState) { - runInJVM(javaVM(), func(env *C.JNIEnv) { - if old.Snippet != new.Snippet { - callVoidMethod(env, w.view, gioView.restartInput) - return - } - if old.Selection.Range != new.Selection.Range { - w.callbacks.SetComposingRegion(key.Range{Start: -1, End: -1}) - callVoidMethod(env, w.view, gioView.updateSelection) - } - if old.Selection.Transform != new.Selection.Transform || old.Selection.Caret != new.Selection.Caret { - sel := new.Selection - m00, m01, m02, m10, m11, m12 := sel.Transform.Elems() - f := func(v float32) jvalue { - return jvalue(math.Float32bits(v)) - } - c := sel.Caret - callVoidMethod(env, w.view, gioView.updateCaret, f(m00), f(m01), f(m02), f(m10), f(m11), f(m12), f(c.Pos.X), f(c.Pos.Y-c.Ascent), f(c.Pos.Y), f(c.Pos.Y+c.Descent)) - } - }) -} - -func (w *window) ShowTextInput(show bool) { - runInJVM(javaVM(), func(env *C.JNIEnv) { - if show { - callVoidMethod(env, w.view, gioView.showTextInput) - } else { - callVoidMethod(env, w.view, gioView.hideTextInput) - } - }) -} - -func (w *window) SetInputHint(mode key.InputHint) { - w.inputHint = mode - - // Constants defined at https://developer.android.com/reference/android/text/InputType. - const ( - TYPE_NULL = 0 - - TYPE_CLASS_TEXT = 1 - TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 32 - TYPE_TEXT_VARIATION_URI = 16 - TYPE_TEXT_VARIATION_PASSWORD = 128 - TYPE_TEXT_FLAG_CAP_SENTENCES = 16384 - TYPE_TEXT_FLAG_AUTO_CORRECT = 32768 - - TYPE_CLASS_NUMBER = 2 - TYPE_NUMBER_FLAG_DECIMAL = 8192 - TYPE_NUMBER_FLAG_SIGNED = 4096 - - TYPE_CLASS_PHONE = 3 - ) - - runInJVM(javaVM(), func(env *C.JNIEnv) { - var m jvalue - switch mode { - case key.HintText: - m = TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_AUTO_CORRECT | TYPE_TEXT_FLAG_CAP_SENTENCES - case key.HintNumeric: - m = TYPE_CLASS_NUMBER | TYPE_NUMBER_FLAG_DECIMAL | TYPE_NUMBER_FLAG_SIGNED - case key.HintEmail: - m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_EMAIL_ADDRESS - case key.HintURL: - m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_URI - case key.HintTelephone: - m = TYPE_CLASS_PHONE - case key.HintPassword: - m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD - default: - m = TYPE_CLASS_TEXT - } - - callVoidMethod(env, w.view, gioView.setInputHint, m) - }) -} - -func javaBool(b bool) C.jboolean { - if b { - return C.JNI_TRUE - } else { - return C.JNI_FALSE - } -} - -func javaString(env *C.JNIEnv, str string) C.jstring { - utf16Chars := utf16.Encode([]rune(str)) - var ptr *C.jchar - if len(utf16Chars) > 0 { - ptr = (*C.jchar)(unsafe.Pointer(&utf16Chars[0])) - } - return C.jni_NewString(env, ptr, C.int(len(utf16Chars))) -} - -func varArgs(args []jvalue) *C.jvalue { - if len(args) == 0 { - return nil - } - return (*C.jvalue)(unsafe.Pointer(&args[0])) -} - -func callStaticVoidMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, args ...jvalue) error { - C.jni_CallStaticVoidMethodA(env, cls, method, varArgs(args)) - return exception(env) -} - -func callStaticObjectMethod(env *C.JNIEnv, cls C.jclass, method C.jmethodID, args ...jvalue) (C.jobject, error) { - res := C.jni_CallStaticObjectMethodA(env, cls, method, varArgs(args)) - return res, exception(env) -} - -func callVoidMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) error { - C.jni_CallVoidMethodA(env, obj, method, varArgs(args)) - return exception(env) -} - -func callBooleanMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) (bool, error) { - res := C.jni_CallBooleanMethodA(env, obj, method, varArgs(args)) - return res == C.JNI_TRUE, exception(env) -} - -func callObjectMethod(env *C.JNIEnv, obj C.jobject, method C.jmethodID, args ...jvalue) (C.jobject, error) { - res := C.jni_CallObjectMethodA(env, obj, method, varArgs(args)) - return res, exception(env) -} - -func newObject(env *C.JNIEnv, cls C.jclass, method C.jmethodID, args ...jvalue) (C.jobject, error) { - res := C.jni_NewObjectA(env, cls, method, varArgs(args)) - return res, exception(env) -} - -// exception returns an error corresponding to the pending -// exception, or nil if no exception is pending. The pending -// exception is cleared. -func exception(env *C.JNIEnv) error { - thr := C.jni_ExceptionOccurred(env) - if thr == 0 { - return nil - } - C.jni_ExceptionClear(env) - cls := getObjectClass(env, C.jobject(thr)) - toString := getMethodID(env, cls, "toString", "()Ljava/lang/String;") - msg, err := callObjectMethod(env, C.jobject(thr), toString) - if err != nil { - return err - } - return errors.New(goString(env, C.jstring(msg))) -} - -func getObjectClass(env *C.JNIEnv, obj C.jobject) C.jclass { - if obj == 0 { - panic("null object") - } - cls := C.jni_GetObjectClass(env, C.jobject(obj)) - if err := exception(env); err != nil { - // GetObjectClass should never fail. - panic(err) - } - return cls -} - -// goString converts the JVM jstring to a Go string. -func goString(env *C.JNIEnv, str C.jstring) string { - if str == 0 { - return "" - } - strlen := C.jni_GetStringLength(env, C.jstring(str)) - chars := C.jni_GetStringChars(env, C.jstring(str)) - utf16Chars := unsafe.Slice((*uint16)(unsafe.Pointer(chars)), strlen) - utf8 := utf16.Decode(utf16Chars) - return string(utf8) -} - -func findClass(env *C.JNIEnv, name string) C.jclass { - cn := C.CString(name) - defer C.free(unsafe.Pointer(cn)) - return C.jni_FindClass(env, cn) -} - -func osMain() { -} - -func newWindow(window *callbacks, options []Option) { - mainWindow.in <- windowAndConfig{window, options} - <-mainWindow.windows -} - -func (w *window) WriteClipboard(mime string, s []byte) { - runInJVM(javaVM(), func(env *C.JNIEnv) { - jstr := javaString(env, string(s)) - callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, - jvalue(android.appCtx), jvalue(jstr)) - }) -} - -func (w *window) ReadClipboard() { - runInJVM(javaVM(), func(env *C.JNIEnv) { - c, err := callStaticObjectMethod(env, android.gioCls, android.mreadClipboard, - jvalue(android.appCtx)) - if err != nil { - return - } - content := goString(env, C.jstring(c)) - w.processEvent(transfer.DataEvent{ - Type: "application/text", - Open: func() io.ReadCloser { - return io.NopCloser(strings.NewReader(content)) - }, - }) - }) -} - -func (w *window) Configure(options []Option) { - cnf := w.config - cnf.apply(unit.Metric{}, options) - runInJVM(javaVM(), func(env *C.JNIEnv) { - w.setConfig(env, cnf) - }) -} - -func (w *window) setConfig(env *C.JNIEnv, cnf Config) { - prev := w.config - // Decorations are never disabled. - cnf.Decorated = true - - if prev.Orientation != cnf.Orientation { - w.config.Orientation = cnf.Orientation - setOrientation(env, w.view, cnf.Orientation) - } - if prev.NavigationColor != cnf.NavigationColor { - w.config.NavigationColor = cnf.NavigationColor - setNavigationColor(env, w.view, cnf.NavigationColor) - } - if prev.StatusColor != cnf.StatusColor { - w.config.StatusColor = cnf.StatusColor - setStatusColor(env, w.view, cnf.StatusColor) - } - if prev.Mode != cnf.Mode { - switch cnf.Mode { - case Fullscreen: - callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE) - w.config.Mode = Fullscreen - case Windowed: - callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE) - w.config.Mode = Windowed - } - } - if cnf.Decorated != prev.Decorated { - w.config.Decorated = cnf.Decorated - } - w.processEvent(ConfigEvent{Config: w.config}) -} - -func (w *window) Perform(system.Action) {} - -func (w *window) SetCursor(cursor pointer.Cursor) { - runInJVM(javaVM(), func(env *C.JNIEnv) { - setCursor(env, w.view, cursor) - }) -} - -func (w *window) wakeup() { - runOnMain(func(env *C.JNIEnv) { - w.loop.Wakeup() - w.loop.FlushEvents() - }) -} - -var androidCursor = [...]uint16{ - pointer.CursorDefault: 1000, // TYPE_ARROW - pointer.CursorNone: 0, - pointer.CursorText: 1008, // TYPE_TEXT - pointer.CursorVerticalText: 1009, // TYPE_VERTICAL_TEXT - pointer.CursorPointer: 1002, // TYPE_HAND - pointer.CursorCrosshair: 1007, // TYPE_CROSSHAIR - pointer.CursorAllScroll: 1013, // TYPE_ALL_SCROLL - pointer.CursorColResize: 1014, // TYPE_HORIZONTAL_DOUBLE_ARROW - pointer.CursorRowResize: 1015, // TYPE_VERTICAL_DOUBLE_ARROW - pointer.CursorGrab: 1020, // TYPE_GRAB - pointer.CursorGrabbing: 1021, // TYPE_GRABBING - pointer.CursorNotAllowed: 1012, // TYPE_NO_DROP - pointer.CursorWait: 1004, // TYPE_WAIT - pointer.CursorProgress: 1000, // TYPE_ARROW - pointer.CursorNorthWestResize: 1017, // TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW - pointer.CursorNorthEastResize: 1016, // TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW - pointer.CursorSouthWestResize: 1016, // TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW - pointer.CursorSouthEastResize: 1017, // TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW - pointer.CursorNorthSouthResize: 1015, // TYPE_VERTICAL_DOUBLE_ARROW - pointer.CursorEastWestResize: 1014, // TYPE_HORIZONTAL_DOUBLE_ARROW - pointer.CursorWestResize: 1014, // TYPE_HORIZONTAL_DOUBLE_ARROW - pointer.CursorEastResize: 1014, // TYPE_HORIZONTAL_DOUBLE_ARROW - pointer.CursorNorthResize: 1015, // TYPE_VERTICAL_DOUBLE_ARROW - pointer.CursorSouthResize: 1015, // TYPE_VERTICAL_DOUBLE_ARROW - pointer.CursorNorthEastSouthWestResize: 1016, // TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW - pointer.CursorNorthWestSouthEastResize: 1017, // TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW -} - -func setCursor(env *C.JNIEnv, view C.jobject, cursor pointer.Cursor) { - curID := androidCursor[cursor] - callVoidMethod(env, view, gioView.setCursor, jvalue(curID)) -} - -func setOrientation(env *C.JNIEnv, view C.jobject, mode Orientation) { - var ( - id int - idFallback int // Used only for SDK 17 or older. - ) - // Constants defined at https://developer.android.com/reference/android/content/pm/ActivityInfo. - switch mode { - case AnyOrientation: - id, idFallback = 2, 2 // SCREEN_ORIENTATION_USER - case LandscapeOrientation: - id, idFallback = 11, 0 // SCREEN_ORIENTATION_USER_LANDSCAPE (or SCREEN_ORIENTATION_LANDSCAPE) - case PortraitOrientation: - id, idFallback = 12, 1 // SCREEN_ORIENTATION_USER_PORTRAIT (or SCREEN_ORIENTATION_PORTRAIT) - } - callVoidMethod(env, view, gioView.setOrientation, jvalue(id), jvalue(idFallback)) -} - -func setStatusColor(env *C.JNIEnv, view C.jobject, color color.NRGBA) { - callVoidMethod(env, view, gioView.setStatusColor, - jvalue(uint32(color.A)<<24|uint32(color.R)<<16|uint32(color.G)<<8|uint32(color.B)), - jvalue(int(f32color.LinearFromSRGB(color).Luminance()*255)), - ) -} - -func setNavigationColor(env *C.JNIEnv, view C.jobject, color color.NRGBA) { - callVoidMethod(env, view, gioView.setNavigationColor, - jvalue(uint32(color.A)<<24|uint32(color.R)<<16|uint32(color.G)<<8|uint32(color.B)), - jvalue(int(f32color.LinearFromSRGB(color).Luminance()*255)), - ) -} - -// runOnMain runs a function on the Java main thread. -func runOnMain(f func(env *C.JNIEnv)) { - go func() { - mainFuncs <- f - runInJVM(javaVM(), func(env *C.JNIEnv) { - callStaticVoidMethod(env, android.gioCls, android.mwakeupMainThread) - }) - }() -} - -//export Java_org_gioui_Gio_scheduleMainFuncs -func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) { - for { - select { - case f := <-mainFuncs: - f(env) - default: - return - } - } -} - -func (AndroidViewEvent) implementsViewEvent() {} -func (AndroidViewEvent) ImplementsEvent() {} -func (a AndroidViewEvent) Valid() bool { - return a != (AndroidViewEvent{}) -} diff --git a/gio/app/os_darwin.go b/gio/app/os_darwin.go deleted file mode 100644 index 7d56fbe..0000000 --- a/gio/app/os_darwin.go +++ /dev/null @@ -1,265 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -/* -#include - -__attribute__ ((visibility ("hidden"))) void gio_runOnMain(uintptr_t h); -__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void); -__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl); -__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl); -__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl); -__attribute__ ((visibility ("hidden"))) void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did); -__attribute__ ((visibility ("hidden"))) void gio_hideCursor(); -__attribute__ ((visibility ("hidden"))) void gio_showCursor(); -__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID); - -static bool isMainThread() { - return [NSThread isMainThread]; -} - -static NSUInteger nsstringLength(CFTypeRef cstr) { - NSString *str = (__bridge NSString *)cstr; - return [str length]; -} - -static void nsstringGetCharacters(CFTypeRef cstr, unichar *chars, NSUInteger loc, NSUInteger length) { - NSString *str = (__bridge NSString *)cstr; - [str getCharacters:chars range:NSMakeRange(loc, length)]; -} - -static CFTypeRef newNSString(unichar *chars, NSUInteger length) { - @autoreleasepool { - NSString *s = [NSString string]; - if (length > 0) { - s = [NSString stringWithCharacters:chars length:length]; - } - return CFBridgingRetain(s); - } -} -*/ -import "C" - -import ( - "errors" - "runtime/cgo" - "sync" - "sync/atomic" - "time" - "unicode/utf16" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/io/pointer" -) - -// displayLink is the state for a display link (CVDisplayLinkRef on macOS, -// CADisplayLink on iOS). It runs a state-machine goroutine that keeps the -// display link running for a while after being stopped to avoid the thread -// start/stop overhead and because the CVDisplayLink sometimes fails to -// start, stop and start again within a short duration. -type displayLink struct { - callback func() - // states is for starting or stopping the display link. - states chan bool - // done is closed when the display link is destroyed. - done chan struct{} - // dids receives the display id when the callback owner is moved - // to a different screen. - dids chan uint64 - // running tracks the desired state of the link. running is accessed - // with atomic. - running uint32 -} - -// displayLinks maps CFTypeRefs to *displayLinks. -var displayLinks sync.Map - -func isMainThread() bool { - return bool(C.isMainThread()) -} - -// runOnMain runs the function on the main thread. -func runOnMain(f func()) { - if isMainThread() { - f() - return - } - C.gio_runOnMain(C.uintptr_t(cgo.NewHandle(f))) -} - -//export gio_runFunc -func gio_runFunc(h C.uintptr_t) { - handle := cgo.Handle(h) - defer handle.Delete() - f := handle.Value().(func()) - f() -} - -// nsstringToString converts a NSString to a Go string. -func nsstringToString(str C.CFTypeRef) string { - if str == 0 { - return "" - } - n := C.nsstringLength(str) - if n == 0 { - return "" - } - chars := make([]uint16, n) - C.nsstringGetCharacters(str, (*C.unichar)(unsafe.Pointer(&chars[0])), 0, n) - utf8 := utf16.Decode(chars) - return string(utf8) -} - -// stringToNSString converts a Go string to a retained NSString. -func stringToNSString(str string) C.CFTypeRef { - u16 := utf16.Encode([]rune(str)) - var chars *C.unichar - if len(u16) > 0 { - chars = (*C.unichar)(unsafe.Pointer(&u16[0])) - } - return C.newNSString(chars, C.NSUInteger(len(u16))) -} - -func newDisplayLink(callback func()) (*displayLink, error) { - d := &displayLink{ - callback: callback, - done: make(chan struct{}), - states: make(chan bool), - dids: make(chan uint64), - } - dl := C.gio_createDisplayLink() - if dl == 0 { - return nil, errors.New("app: failed to create display link") - } - go d.run(dl) - return d, nil -} - -func (d *displayLink) run(dl C.CFTypeRef) { - defer C.gio_releaseDisplayLink(dl) - displayLinks.Store(dl, d) - defer displayLinks.Delete(dl) - var stopTimer *time.Timer - var tchan <-chan time.Time - started := false - for { - select { - case <-tchan: - tchan = nil - started = false - C.gio_stopDisplayLink(dl) - case start := <-d.states: - switch { - case !start && tchan == nil: - // stopTimeout is the delay before stopping the display link to - // avoid the overhead of frequently starting and stopping the - // link thread. - const stopTimeout = 500 * time.Millisecond - if stopTimer == nil { - stopTimer = time.NewTimer(stopTimeout) - } else { - // stopTimer is always drained when tchan == nil. - stopTimer.Reset(stopTimeout) - } - tchan = stopTimer.C - atomic.StoreUint32(&d.running, 0) - case start: - if tchan != nil && !stopTimer.Stop() { - <-tchan - } - tchan = nil - atomic.StoreUint32(&d.running, 1) - if !started { - started = true - C.gio_startDisplayLink(dl) - } - } - case did := <-d.dids: - C.gio_setDisplayLinkDisplay(dl, C.uint64_t(did)) - case <-d.done: - return - } - } -} - -func (d *displayLink) Start() { - d.states <- true -} - -func (d *displayLink) Stop() { - d.states <- false -} - -func (d *displayLink) Close() { - close(d.done) -} - -func (d *displayLink) SetDisplayID(did uint64) { - d.dids <- did -} - -//export gio_onFrameCallback -func gio_onFrameCallback(ref C.CFTypeRef) { - d, exists := displayLinks.Load(ref) - if !exists { - return - } - dl := d.(*displayLink) - if atomic.LoadUint32(&dl.running) != 0 { - dl.callback() - } -} - -var macosCursorID = [...]byte{ - pointer.CursorDefault: 0, - pointer.CursorNone: 1, - pointer.CursorText: 2, - pointer.CursorVerticalText: 3, - pointer.CursorPointer: 4, - pointer.CursorCrosshair: 5, - pointer.CursorAllScroll: 6, - pointer.CursorColResize: 7, - pointer.CursorRowResize: 8, - pointer.CursorGrab: 9, - pointer.CursorGrabbing: 10, - pointer.CursorNotAllowed: 11, - pointer.CursorWait: 12, - pointer.CursorProgress: 13, - pointer.CursorNorthWestResize: 14, - pointer.CursorNorthEastResize: 15, - pointer.CursorSouthWestResize: 16, - pointer.CursorSouthEastResize: 17, - pointer.CursorNorthSouthResize: 18, - pointer.CursorEastWestResize: 19, - pointer.CursorWestResize: 20, - pointer.CursorEastResize: 21, - pointer.CursorNorthResize: 22, - pointer.CursorSouthResize: 23, - pointer.CursorNorthEastSouthWestResize: 24, - pointer.CursorNorthWestSouthEastResize: 25, -} - -// windowSetCursor updates the cursor from the current one to a new one -// and returns the new one. -func windowSetCursor(from, to pointer.Cursor) pointer.Cursor { - if from == to { - return to - } - if to == pointer.CursorNone { - C.gio_hideCursor() - return to - } - if from == pointer.CursorNone { - C.gio_showCursor() - } - C.gio_setCursor(C.NSUInteger(macosCursorID[to])) - return to -} - -func (w *window) wakeup() { - runOnMain(func() { - w.loop.Wakeup() - w.loop.FlushEvents() - }) -} diff --git a/gio/app/os_ios.go b/gio/app/os_ios.go deleted file mode 100644 index a3fbd47..0000000 --- a/gio/app/os_ios.go +++ /dev/null @@ -1,446 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build darwin && ios -// +build darwin,ios - -package app - -/* -#cgo CFLAGS: -DGLES_SILENCE_DEPRECATION -Werror -Wno-deprecated-declarations -fmodules -fobjc-arc -x objective-c - -#include -#include -#include - -__attribute__ ((visibility ("hidden"))) int gio_applicationMain(int argc, char *argv[]); -__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle); - -struct drawParams { - CGFloat dpi, sdpi; - CGFloat width, height; - CGFloat top, right, bottom, left; -}; - -static void writeClipboard(unichar *chars, NSUInteger length) { -#if !TARGET_OS_TV - @autoreleasepool { - NSString *s = [NSString string]; - if (length > 0) { - s = [NSString stringWithCharacters:chars length:length]; - } - UIPasteboard *p = UIPasteboard.generalPasteboard; - p.string = s; - } -#endif -} - -static CFTypeRef readClipboard(void) { -#if !TARGET_OS_TV - @autoreleasepool { - UIPasteboard *p = UIPasteboard.generalPasteboard; - return (__bridge_retained CFTypeRef)p.string; - } -#else - return nil; -#endif -} - -static void showTextInput(CFTypeRef viewRef) { - UIView *view = (__bridge UIView *)viewRef; - [view becomeFirstResponder]; -} - -static void hideTextInput(CFTypeRef viewRef) { - UIView *view = (__bridge UIView *)viewRef; - [view resignFirstResponder]; -} - -static struct drawParams viewDrawParams(CFTypeRef viewRef) { - UIView *v = (__bridge UIView *)viewRef; - struct drawParams params; - CGFloat scale = v.layer.contentsScale; - // Use 163 as the standard ppi on iOS. - params.dpi = 163*scale; - params.sdpi = params.dpi; - UIEdgeInsets insets = v.layoutMargins; - if (@available(iOS 11.0, tvOS 11.0, *)) { - UIFontMetrics *metrics = [UIFontMetrics defaultMetrics]; - params.sdpi = [metrics scaledValueForValue:params.sdpi]; - insets = v.safeAreaInsets; - } - params.width = v.bounds.size.width*scale; - params.height = v.bounds.size.height*scale; - params.top = insets.top*scale; - params.right = insets.right*scale; - params.bottom = insets.bottom*scale; - params.left = insets.left*scale; - return params; -} -*/ -import "C" - -import ( - "image" - "io" - "os" - "runtime" - "runtime/cgo" - "runtime/debug" - "strings" - "time" - "unicode/utf16" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -type UIKitViewEvent struct { - // ViewController is a CFTypeRef for the UIViewController backing a Window. - ViewController uintptr -} - -type window struct { - view C.CFTypeRef - w *callbacks - displayLink *displayLink - loop *eventLoop - - hidden bool - cursor pointer.Cursor - config Config - - pointerMap []C.CFTypeRef -} - -var mainWindow = newWindowRendezvous() - -func init() { - // Darwin requires UI operations happen on the main thread only. - runtime.LockOSThread() -} - -//export onCreate -func onCreate(view, controller C.CFTypeRef) { - wopts := <-mainWindow.out - w := &window{ - view: view, - w: wopts.window, - } - w.loop = newEventLoop(w.w, w.wakeup) - w.w.SetDriver(w) - mainWindow.windows <- struct{}{} - dl, err := newDisplayLink(func() { - w.draw(false) - }) - if err != nil { - w.w.ProcessEvent(DestroyEvent{Err: err}) - return - } - w.displayLink = dl - C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w))) - w.Configure(wopts.options) - w.ProcessEvent(UIKitViewEvent{ViewController: uintptr(controller)}) -} - -func viewFor(h C.uintptr_t) *window { - return cgo.Handle(h).Value().(*window) -} - -//export gio_onDraw -func gio_onDraw(h C.uintptr_t) { - w := viewFor(h) - w.draw(true) -} - -func (w *window) draw(sync bool) { - if w.hidden { - return - } - params := C.viewDrawParams(w.view) - if params.width == 0 || params.height == 0 { - return - } - const inchPrDp = 1.0 / 163 - m := unit.Metric{ - PxPerDp: float32(params.dpi) * inchPrDp, - PxPerSp: float32(params.sdpi) * inchPrDp, - } - dppp := unit.Dp(1. / m.PxPerDp) - w.ProcessEvent(frameEvent{ - FrameEvent: FrameEvent{ - Now: time.Now(), - Size: image.Point{ - X: int(params.width + .5), - Y: int(params.height + .5), - }, - Insets: Insets{ - Top: unit.Dp(params.top) * dppp, - Bottom: unit.Dp(params.bottom) * dppp, - Left: unit.Dp(params.left) * dppp, - Right: unit.Dp(params.right) * dppp, - }, - Metric: m, - }, - Sync: sync, - }) -} - -//export onStop -func onStop(h C.uintptr_t) { - w := viewFor(h) - w.hidden = true -} - -//export onStart -func onStart(h C.uintptr_t) { - w := viewFor(h) - w.hidden = false - w.draw(true) -} - -//export onDestroy -func onDestroy(h C.uintptr_t) { - w := viewFor(h) - w.ProcessEvent(UIKitViewEvent{}) - w.ProcessEvent(DestroyEvent{}) - w.displayLink.Close() - w.displayLink = nil - cgo.Handle(h).Delete() - w.view = 0 -} - -//export onFocus -func onFocus(h C.uintptr_t, focus int) { - w := viewFor(h) - w.config.Focused = focus != 0 - w.ProcessEvent(ConfigEvent{Config: w.config}) -} - -//export onLowMemory -func onLowMemory() { - runtime.GC() - debug.FreeOSMemory() -} - -//export onUpArrow -func onUpArrow(h C.uintptr_t) { - viewFor(h).onKeyCommand(key.NameUpArrow) -} - -//export onDownArrow -func onDownArrow(h C.uintptr_t) { - viewFor(h).onKeyCommand(key.NameDownArrow) -} - -//export onLeftArrow -func onLeftArrow(h C.uintptr_t) { - viewFor(h).onKeyCommand(key.NameLeftArrow) -} - -//export onRightArrow -func onRightArrow(h C.uintptr_t) { - viewFor(h).onKeyCommand(key.NameRightArrow) -} - -//export onDeleteBackward -func onDeleteBackward(h C.uintptr_t) { - viewFor(h).onKeyCommand(key.NameDeleteBackward) -} - -//export onText -func onText(h C.uintptr_t, str C.CFTypeRef) { - w := viewFor(h) - w.w.EditorInsert(nsstringToString(str)) -} - -//export onTouch -func onTouch(h C.uintptr_t, last C.int, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) { - var kind pointer.Kind - switch phase { - case C.UITouchPhaseBegan: - kind = pointer.Press - case C.UITouchPhaseMoved: - kind = pointer.Move - case C.UITouchPhaseEnded: - kind = pointer.Release - case C.UITouchPhaseCancelled: - kind = pointer.Cancel - default: - return - } - w := viewFor(h) - t := time.Duration(float64(ti) * float64(time.Second)) - p := f32.Point{X: float32(x), Y: float32(y)} - w.ProcessEvent(pointer.Event{ - Kind: kind, - Source: pointer.Touch, - PointerID: w.lookupTouch(last != 0, touchRef), - Position: p, - Time: t, - }) -} - -func (w *window) ReadClipboard() { - cstr := C.readClipboard() - defer C.CFRelease(cstr) - content := nsstringToString(cstr) - w.ProcessEvent(transfer.DataEvent{ - Type: "application/text", - Open: func() io.ReadCloser { - return io.NopCloser(strings.NewReader(content)) - }, - }) -} - -func (w *window) WriteClipboard(mime string, s []byte) { - u16 := utf16.Encode([]rune(string(s))) - var chars *C.unichar - if len(u16) > 0 { - chars = (*C.unichar)(unsafe.Pointer(&u16[0])) - } - C.writeClipboard(chars, C.NSUInteger(len(u16))) -} - -func (w *window) Configure([]Option) { - // Decorations are never disabled. - w.config.Decorated = true - w.ProcessEvent(ConfigEvent{Config: w.config}) -} - -func (w *window) EditorStateChanged(old, new editorState) {} - -func (w *window) Perform(system.Action) {} - -func (w *window) SetAnimating(anim bool) { - if anim { - w.displayLink.Start() - } else { - w.displayLink.Stop() - } -} - -func (w *window) SetCursor(cursor pointer.Cursor) { - w.cursor = windowSetCursor(w.cursor, cursor) -} - -func (w *window) onKeyCommand(name key.Name) { - w.ProcessEvent(key.Event{ - Name: name, - }) -} - -// lookupTouch maps an UITouch pointer value to an index. If -// last is set, the map is cleared. -func (w *window) lookupTouch(last bool, touch C.CFTypeRef) pointer.ID { - id := -1 - for i, ref := range w.pointerMap { - if ref == touch { - id = i - break - } - } - if id == -1 { - id = len(w.pointerMap) - w.pointerMap = append(w.pointerMap, touch) - } - if last { - w.pointerMap = w.pointerMap[:0] - } - return pointer.ID(id) -} - -func (w *window) contextView() C.CFTypeRef { - return w.view -} - -func (w *window) ShowTextInput(show bool) { - if show { - C.showTextInput(w.view) - } else { - C.hideTextInput(w.view) - } -} - -func (w *window) SetInputHint(_ key.InputHint) {} - -func (w *window) ProcessEvent(e event.Event) { - w.w.ProcessEvent(e) - w.loop.FlushEvents() -} - -func (w *window) Event() event.Event { - return w.loop.Event() -} - -func (w *window) Invalidate() { - w.loop.Invalidate() -} - -func (w *window) Run(f func()) { - w.loop.Run(f) -} - -func (w *window) Frame(frame *op.Ops) { - w.loop.Frame(frame) -} - -func newWindow(win *callbacks, options []Option) { - mainWindow.in <- windowAndConfig{win, options} - <-mainWindow.windows -} - -var mainMode = mainModeUndefined - -const ( - mainModeUndefined = iota - mainModeExe - mainModeLibrary -) - -func osMain() { - if !isMainThread() { - panic("app.Main must be run on the main goroutine") - } - switch mainMode { - case mainModeUndefined: - mainMode = mainModeExe - var argv []*C.char - for _, arg := range os.Args { - a := C.CString(arg) - defer C.free(unsafe.Pointer(a)) - argv = append(argv, a) - } - C.gio_applicationMain(C.int(len(argv)), unsafe.SliceData(argv)) - case mainModeExe: - panic("app.Main may be called only once") - case mainModeLibrary: - // Do nothing, we're embedded as a library. - } -} - -//export gio_runMain -func gio_runMain() { - if !isMainThread() { - panic("app.Main must be run on the main goroutine") - } - switch mainMode { - case mainModeUndefined: - mainMode = mainModeLibrary - runMain() - case mainModeExe: - // Do nothing, main has already been called. - } -} - -func (UIKitViewEvent) implementsViewEvent() {} -func (UIKitViewEvent) ImplementsEvent() {} -func (u UIKitViewEvent) Valid() bool { - return u != (UIKitViewEvent{}) -} diff --git a/gio/app/os_js.go b/gio/app/os_js.go deleted file mode 100644 index a8c60df..0000000 --- a/gio/app/os_js.go +++ /dev/null @@ -1,827 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "fmt" - "image" - "image/color" - "io" - "strings" - "syscall/js" - "time" - "unicode" - "unicode/utf8" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/op" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -type JSViewEvent struct { - Element js.Value -} - -type contextStatus int - -const ( - contextStatusOkay contextStatus = iota - contextStatusLost - contextStatusRestored -) - -type window struct { - window js.Value - document js.Value - head js.Value - clipboard js.Value - cnv js.Value - tarea js.Value - w *callbacks - redraw js.Func - clipboardCallback js.Func - requestAnimationFrame js.Value - browserHistory js.Value - visualViewport js.Value - screenOrientation js.Value - cleanfuncs []func() - touches []js.Value - composing bool - requestFocus bool - - config Config - inset f32.Point - scale float32 - animating bool - // animRequested tracks whether a requestAnimationFrame callback - // is pending. - animRequested bool - wakeups chan struct{} - - contextStatus contextStatus -} - -func newWindow(win *callbacks, options []Option) { - doc := js.Global().Get("document") - cont := getContainer(doc) - cnv := createCanvas(doc) - cont.Call("appendChild", cnv) - tarea := createTextArea(doc) - cont.Call("appendChild", tarea) - w := &window{ - cnv: cnv, - document: doc, - tarea: tarea, - window: js.Global().Get("window"), - head: doc.Get("head"), - clipboard: js.Global().Get("navigator").Get("clipboard"), - wakeups: make(chan struct{}, 1), - w: win, - } - w.w.SetDriver(w) - w.requestAnimationFrame = w.window.Get("requestAnimationFrame") - w.browserHistory = w.window.Get("history") - w.visualViewport = w.window.Get("visualViewport") - if w.visualViewport.IsUndefined() { - w.visualViewport = w.window - } - if screen := w.window.Get("screen"); screen.Truthy() { - w.screenOrientation = screen.Get("orientation") - } - w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} { - w.draw(false) - return nil - }) - w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} { - content := args[0].String() - w.processEvent(transfer.DataEvent{ - Type: "application/text", - Open: func() io.ReadCloser { - return io.NopCloser(strings.NewReader(content)) - }, - }) - return nil - }) - w.addEventListeners() - w.addHistory() - - w.Configure(options) - w.blur() - w.processEvent(JSViewEvent{Element: cont}) - w.resize() - w.draw(true) -} - -func getContainer(doc js.Value) js.Value { - cont := doc.Call("getElementById", "giowindow") - if !cont.IsNull() { - return cont - } - cont = doc.Call("createElement", "DIV") - doc.Get("body").Call("appendChild", cont) - return cont -} - -func createTextArea(doc js.Value) js.Value { - tarea := doc.Call("createElement", "input") - style := tarea.Get("style") - style.Set("width", "1px") - style.Set("height", "1px") - style.Set("opacity", "0") - style.Set("border", "0") - style.Set("padding", "0") - tarea.Set("autocomplete", "off") - tarea.Set("autocorrect", "off") - tarea.Set("autocapitalize", "off") - tarea.Set("spellcheck", false) - return tarea -} - -func createCanvas(doc js.Value) js.Value { - cnv := doc.Call("createElement", "canvas") - style := cnv.Get("style") - style.Set("position", "fixed") - style.Set("width", "100%") - style.Set("height", "100%") - return cnv -} - -func (w *window) cleanup() { - // Cleanup in the opposite order of - // construction. - for i := len(w.cleanfuncs) - 1; i >= 0; i-- { - w.cleanfuncs[i]() - } - w.cleanfuncs = nil -} - -func (w *window) addEventListeners() { - w.addEventListener(w.cnv, "webglcontextlost", func(this js.Value, args []js.Value) interface{} { - args[0].Call("preventDefault") - w.contextStatus = contextStatusLost - return nil - }) - w.addEventListener(w.cnv, "webglcontextrestored", func(this js.Value, args []js.Value) interface{} { - args[0].Call("preventDefault") - w.contextStatus = contextStatusRestored - - // Resize is required to force update the canvas content when restored. - w.cnv.Set("width", 0) - w.cnv.Set("height", 0) - w.resize() - w.draw(true) - return nil - }) - w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} { - w.resize() - w.draw(true) - return nil - }) - w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} { - args[0].Call("preventDefault") - return nil - }) - w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} { - if w.processEvent(key.Event{Name: key.NameBack}) { - return w.browserHistory.Call("forward") - } - return w.browserHistory.Call("back") - }) - w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} { - w.pointerEvent(pointer.Move, 0, 0, args[0]) - return nil - }) - w.addEventListener(w.cnv, "mousedown", func(this js.Value, args []js.Value) interface{} { - w.pointerEvent(pointer.Press, 0, 0, args[0]) - if w.requestFocus { - w.focus() - w.requestFocus = false - } - return nil - }) - w.addEventListener(w.cnv, "mouseup", func(this js.Value, args []js.Value) interface{} { - w.pointerEvent(pointer.Release, 0, 0, args[0]) - return nil - }) - w.addEventListener(w.cnv, "wheel", func(this js.Value, args []js.Value) interface{} { - e := args[0] - dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float() - // horizontal scroll if shift is pressed. - if e.Get("shiftKey").Bool() { - dx, dy = dy, dx - } - mode := e.Get("deltaMode").Int() - switch mode { - case 0x01: // DOM_DELTA_LINE - dx *= 10 - dy *= 10 - case 0x02: // DOM_DELTA_PAGE - dx *= 120 - dy *= 120 - } - w.pointerEvent(pointer.Scroll, float32(dx), float32(dy), e) - return nil - }) - w.addEventListener(w.cnv, "touchstart", func(this js.Value, args []js.Value) interface{} { - w.touchEvent(pointer.Press, args[0]) - if w.requestFocus { - w.focus() // iOS can only focus inside a Touch event. - w.requestFocus = false - } - return nil - }) - w.addEventListener(w.cnv, "touchend", func(this js.Value, args []js.Value) interface{} { - w.touchEvent(pointer.Release, args[0]) - return nil - }) - w.addEventListener(w.cnv, "touchmove", func(this js.Value, args []js.Value) interface{} { - w.touchEvent(pointer.Move, args[0]) - return nil - }) - w.addEventListener(w.cnv, "touchcancel", func(this js.Value, args []js.Value) interface{} { - // Cancel all touches even if only one touch was cancelled. - for i := range w.touches { - w.touches[i] = js.Null() - } - w.touches = w.touches[:0] - w.processEvent(pointer.Event{ - Kind: pointer.Cancel, - Source: pointer.Touch, - }) - return nil - }) - w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} { - w.config.Focused = true - w.processEvent(ConfigEvent{Config: w.config}) - return nil - }) - w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} { - w.config.Focused = false - w.processEvent(ConfigEvent{Config: w.config}) - w.blur() - return nil - }) - w.addEventListener(w.tarea, "keydown", func(this js.Value, args []js.Value) interface{} { - w.keyEvent(args[0], key.Press) - return nil - }) - w.addEventListener(w.tarea, "keyup", func(this js.Value, args []js.Value) interface{} { - w.keyEvent(args[0], key.Release) - return nil - }) - w.addEventListener(w.tarea, "compositionstart", func(this js.Value, args []js.Value) interface{} { - w.composing = true - return nil - }) - w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} { - w.composing = false - w.flushInput() - return nil - }) - w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} { - if w.composing { - return nil - } - w.flushInput() - return nil - }) - w.addEventListener(w.tarea, "paste", func(this js.Value, args []js.Value) interface{} { - if w.clipboard.IsUndefined() { - return nil - } - // Prevents duplicated-paste, since "paste" is already handled through Clipboard API. - args[0].Call("preventDefault") - return nil - }) -} - -func (w *window) addHistory() { - w.browserHistory.Call("pushState", nil, nil, w.window.Get("location").Get("href")) -} - -func (w *window) flushInput() { - val := w.tarea.Get("value").String() - w.tarea.Set("value", "") - w.w.EditorInsert(string(val)) -} - -func (w *window) blur() { - w.tarea.Call("blur") - w.requestFocus = false -} - -func (w *window) focus() { - w.tarea.Call("focus") - w.requestFocus = true -} - -func (w *window) keyboard(hint key.InputHint) { - var m string - switch hint { - case key.HintAny: - m = "text" - case key.HintText: - m = "text" - case key.HintNumeric: - m = "decimal" - case key.HintEmail: - m = "email" - case key.HintURL: - m = "url" - case key.HintTelephone: - m = "tel" - case key.HintPassword: - m = "password" - default: - m = "text" - } - w.tarea.Set("inputMode", m) -} - -func (w *window) keyEvent(e js.Value, ks key.State) { - k := e.Get("key").String() - if n, ok := translateKey(k); ok { - cmd := key.Event{ - Name: n, - Modifiers: modifiersFor(e), - State: ks, - } - w.processEvent(cmd) - } -} - -func (w *window) ProcessEvent(e event.Event) { - w.processEvent(e) -} - -func (w *window) processEvent(e event.Event) bool { - if !w.w.ProcessEvent(e) { - return false - } - select { - case w.wakeups <- struct{}{}: - default: - } - return true -} - -func (w *window) Event() event.Event { - for { - evt, ok := w.w.nextEvent() - if ok { - if _, destroy := evt.(DestroyEvent); destroy { - w.cleanup() - } - return evt - } - <-w.wakeups - } -} - -func (w *window) Invalidate() { - w.w.Invalidate() -} - -func (w *window) Run(f func()) { - f() -} - -func (w *window) Frame(frame *op.Ops) { - w.w.ProcessFrame(frame, nil) -} - -// modifiersFor returns the modifier set for a DOM MouseEvent or -// KeyEvent. -func modifiersFor(e js.Value) key.Modifiers { - var mods key.Modifiers - if e.Get("getModifierState").IsUndefined() { - // Some browsers doesn't support getModifierState. - return mods - } - if e.Call("getModifierState", "Alt").Bool() { - mods |= key.ModAlt - } - if e.Call("getModifierState", "Control").Bool() { - mods |= key.ModCtrl - } - if e.Call("getModifierState", "Shift").Bool() { - mods |= key.ModShift - } - return mods -} - -func (w *window) touchEvent(kind pointer.Kind, e js.Value) { - e.Call("preventDefault") - t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond - changedTouches := e.Get("changedTouches") - n := changedTouches.Length() - rect := w.cnv.Call("getBoundingClientRect") - scale := w.scale - var mods key.Modifiers - if e.Get("shiftKey").Bool() { - mods |= key.ModShift - } - if e.Get("altKey").Bool() { - mods |= key.ModAlt - } - if e.Get("ctrlKey").Bool() { - mods |= key.ModCtrl - } - for i := 0; i < n; i++ { - touch := changedTouches.Index(i) - pid := w.touchIDFor(touch) - x, y := touch.Get("clientX").Float(), touch.Get("clientY").Float() - x -= rect.Get("left").Float() - y -= rect.Get("top").Float() - pos := f32.Point{ - X: float32(x) * scale, - Y: float32(y) * scale, - } - w.processEvent(pointer.Event{ - Kind: kind, - Source: pointer.Touch, - Position: pos, - PointerID: pid, - Time: t, - Modifiers: mods, - }) - } -} - -func (w *window) touchIDFor(touch js.Value) pointer.ID { - id := touch.Get("identifier") - for i, id2 := range w.touches { - if id2.Equal(id) { - return pointer.ID(i) - } - } - pid := pointer.ID(len(w.touches)) - w.touches = append(w.touches, id) - return pid -} - -func (w *window) pointerEvent(kind pointer.Kind, dx, dy float32, e js.Value) { - e.Call("preventDefault") - x, y := e.Get("clientX").Float(), e.Get("clientY").Float() - rect := w.cnv.Call("getBoundingClientRect") - x -= rect.Get("left").Float() - y -= rect.Get("top").Float() - scale := w.scale - pos := f32.Point{ - X: float32(x) * scale, - Y: float32(y) * scale, - } - scroll := f32.Point{ - X: dx * scale, - Y: dy * scale, - } - t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond - jbtns := e.Get("buttons").Int() - var btns pointer.Buttons - if jbtns&1 != 0 { - btns |= pointer.ButtonPrimary - } - if jbtns&2 != 0 { - btns |= pointer.ButtonSecondary - } - if jbtns&4 != 0 { - btns |= pointer.ButtonTertiary - } - w.processEvent(pointer.Event{ - Kind: kind, - Source: pointer.Mouse, - Buttons: btns, - Position: pos, - Scroll: scroll, - Time: t, - Modifiers: modifiersFor(e), - }) -} - -func (w *window) addEventListener(this js.Value, event string, f func(this js.Value, args []js.Value) interface{}) { - jsf := w.funcOf(f) - this.Call("addEventListener", event, jsf) - w.cleanfuncs = append(w.cleanfuncs, func() { - this.Call("removeEventListener", event, jsf) - }) -} - -// funcOf is like js.FuncOf but adds the js.Func to a list of -// functions to be released during cleanup. -func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.Func { - jsf := js.FuncOf(f) - w.cleanfuncs = append(w.cleanfuncs, jsf.Release) - return jsf -} - -func (w *window) EditorStateChanged(old, new editorState) {} - -func (w *window) SetAnimating(anim bool) { - w.animating = anim - if anim && !w.animRequested { - w.animRequested = true - w.requestAnimationFrame.Invoke(w.redraw) - } -} - -func (w *window) ReadClipboard() { - if w.clipboard.IsUndefined() { - return - } - if w.clipboard.Get("readText").IsUndefined() { - return - } - w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback) -} - -func (w *window) WriteClipboard(mime string, s []byte) { - if w.clipboard.IsUndefined() { - return - } - if w.clipboard.Get("writeText").IsUndefined() { - return - } - w.clipboard.Call("writeText", string(s)) -} - -func (w *window) Configure(options []Option) { - prev := w.config - cnf := w.config - cnf.apply(unit.Metric{}, options) - // Decorations are never disabled. - cnf.Decorated = true - - if prev.Title != cnf.Title { - w.config.Title = cnf.Title - w.document.Set("title", cnf.Title) - } - if prev.Mode != cnf.Mode { - w.windowMode(cnf.Mode) - } - if prev.NavigationColor != cnf.NavigationColor { - w.config.NavigationColor = cnf.NavigationColor - w.navigationColor(cnf.NavigationColor) - } - if prev.Orientation != cnf.Orientation { - w.config.Orientation = cnf.Orientation - w.orientation(cnf.Orientation) - } - if cnf.Decorated != prev.Decorated { - w.config.Decorated = cnf.Decorated - } - w.processEvent(ConfigEvent{Config: w.config}) -} - -func (w *window) Perform(system.Action) {} - -var webCursor = [...]string{ - pointer.CursorDefault: "default", - pointer.CursorNone: "none", - pointer.CursorText: "text", - pointer.CursorVerticalText: "vertical-text", - pointer.CursorPointer: "pointer", - pointer.CursorCrosshair: "crosshair", - pointer.CursorAllScroll: "all-scroll", - pointer.CursorColResize: "col-resize", - pointer.CursorRowResize: "row-resize", - pointer.CursorGrab: "grab", - pointer.CursorGrabbing: "grabbing", - pointer.CursorNotAllowed: "not-allowed", - pointer.CursorWait: "wait", - pointer.CursorProgress: "progress", - pointer.CursorNorthWestResize: "nw-resize", - pointer.CursorNorthEastResize: "ne-resize", - pointer.CursorSouthWestResize: "sw-resize", - pointer.CursorSouthEastResize: "se-resize", - pointer.CursorNorthSouthResize: "ns-resize", - pointer.CursorEastWestResize: "ew-resize", - pointer.CursorWestResize: "w-resize", - pointer.CursorEastResize: "e-resize", - pointer.CursorNorthResize: "n-resize", - pointer.CursorSouthResize: "s-resize", - pointer.CursorNorthEastSouthWestResize: "nesw-resize", - pointer.CursorNorthWestSouthEastResize: "nwse-resize", -} - -func (w *window) SetCursor(cursor pointer.Cursor) { - style := w.cnv.Get("style") - style.Set("cursor", webCursor[cursor]) -} - -func (w *window) ShowTextInput(show bool) { - // Run in a goroutine to avoid a deadlock if the - // focus change result in an event. - if show { - w.focus() - } else { - w.blur() - } -} - -func (w *window) SetInputHint(mode key.InputHint) { - w.keyboard(mode) -} - -func (w *window) resize() { - w.scale = float32(w.window.Get("devicePixelRatio").Float()) - - rect := w.cnv.Call("getBoundingClientRect") - size := image.Point{ - X: int(float32(rect.Get("width").Float()) * w.scale), - Y: int(float32(rect.Get("height").Float()) * w.scale), - } - if size != w.config.Size { - w.config.Size = size - w.processEvent(ConfigEvent{Config: w.config}) - } - - if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() { - w.inset.X = float32(w.config.Size.X) - float32(vx.Float())*w.scale - w.inset.Y = float32(w.config.Size.Y) - float32(vy.Float())*w.scale - } - - if w.config.Size.X == 0 || w.config.Size.Y == 0 { - return - } - - w.cnv.Set("width", w.config.Size.X) - w.cnv.Set("height", w.config.Size.Y) -} - -func (w *window) draw(sync bool) { - if w.contextStatus == contextStatusLost { - return - } - anim := w.animating - w.animRequested = anim - if anim { - w.requestAnimationFrame.Invoke(w.redraw) - } else if !sync { - return - } - size, insets, metric := w.getConfig() - if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 { - return - } - - w.processEvent(frameEvent{ - FrameEvent: FrameEvent{ - Now: time.Now(), - Size: size, - Insets: insets, - Metric: metric, - }, - Sync: sync, - }) -} - -func (w *window) getConfig() (image.Point, Insets, unit.Metric) { - invscale := unit.Dp(1. / w.scale) - return image.Pt(w.config.Size.X, w.config.Size.Y), - Insets{ - Bottom: unit.Dp(w.inset.Y) * invscale, - Right: unit.Dp(w.inset.X) * invscale, - }, unit.Metric{ - PxPerDp: w.scale, - PxPerSp: w.scale, - } -} - -func (w *window) windowMode(mode WindowMode) { - switch mode { - case Windowed: - if !w.document.Get("fullscreenElement").Truthy() { - return // Browser is already Windowed. - } - if !w.document.Get("exitFullscreen").Truthy() { - return // Browser doesn't support such feature. - } - w.document.Call("exitFullscreen") - w.config.Mode = Windowed - case Fullscreen: - elem := w.document.Get("documentElement") - if !elem.Get("requestFullscreen").Truthy() { - return // Browser doesn't support such feature. - } - elem.Call("requestFullscreen") - w.config.Mode = Fullscreen - } -} - -func (w *window) orientation(mode Orientation) { - if j := w.screenOrientation; !j.Truthy() || !j.Get("unlock").Truthy() || !j.Get("lock").Truthy() { - return // Browser don't support Screen Orientation API. - } - - switch mode { - case AnyOrientation: - w.screenOrientation.Call("unlock") - case LandscapeOrientation: - w.screenOrientation.Call("lock", "landscape").Call("then", w.redraw) - case PortraitOrientation: - w.screenOrientation.Call("lock", "portrait").Call("then", w.redraw) - } -} - -func (w *window) navigationColor(c color.NRGBA) { - theme := w.head.Call("querySelector", `meta[name="theme-color"]`) - if !theme.Truthy() { - theme = w.document.Call("createElement", "meta") - theme.Set("name", "theme-color") - w.head.Call("appendChild", theme) - } - rgba := f32color.NRGBAToRGBA(c) - theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B})) -} - -func osMain() { - select {} -} - -func translateKey(k string) (key.Name, bool) { - var n key.Name - - switch k { - case "ArrowUp": - n = key.NameUpArrow - case "ArrowDown": - n = key.NameDownArrow - case "ArrowLeft": - n = key.NameLeftArrow - case "ArrowRight": - n = key.NameRightArrow - case "Escape": - n = key.NameEscape - case "Enter": - n = key.NameReturn - case "Backspace": - n = key.NameDeleteBackward - case "Delete": - n = key.NameDeleteForward - case "Home": - n = key.NameHome - case "End": - n = key.NameEnd - case "PageUp": - n = key.NamePageUp - case "PageDown": - n = key.NamePageDown - case "Tab": - n = key.NameTab - case " ": - n = key.NameSpace - case "F1": - n = key.NameF1 - case "F2": - n = key.NameF2 - case "F3": - n = key.NameF3 - case "F4": - n = key.NameF4 - case "F5": - n = key.NameF5 - case "F6": - n = key.NameF6 - case "F7": - n = key.NameF7 - case "F8": - n = key.NameF8 - case "F9": - n = key.NameF9 - case "F10": - n = key.NameF10 - case "F11": - n = key.NameF11 - case "F12": - n = key.NameF12 - case "Control": - n = key.NameCtrl - case "Shift": - n = key.NameShift - case "Alt": - n = key.NameAlt - case "OS": - n = key.NameSuper - default: - r, s := utf8.DecodeRuneInString(k) - // If there is exactly one printable character, return that. - if s == len(k) && unicode.IsPrint(r) { - return key.Name(strings.ToUpper(k)), true - } - return "", false - } - return n, true -} - -func (JSViewEvent) implementsViewEvent() {} -func (JSViewEvent) ImplementsEvent() {} -func (j JSViewEvent) Valid() bool { - return !(j.Element.IsNull() || j.Element.IsUndefined()) -} diff --git a/gio/app/os_macos.go b/gio/app/os_macos.go deleted file mode 100644 index 97bddaa..0000000 --- a/gio/app/os_macos.go +++ /dev/null @@ -1,1172 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build darwin && !ios -// +build darwin,!ios - -package app - -import ( - "errors" - "image" - "io" - "runtime" - "runtime/cgo" - "strings" - "time" - "unicode" - "unicode/utf8" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" - - _ "github.com/p9c/p9/pkg/gel/gio/internal/cocoainit" -) - -/* -#cgo CFLAGS: -Werror -Wno-deprecated-declarations -fobjc-arc -x objective-c -#cgo LDFLAGS: -framework AppKit -framework QuartzCore - -#include - -#define MOUSE_MOVE 1 -#define MOUSE_UP 2 -#define MOUSE_DOWN 3 -#define MOUSE_SCROLL 4 - -__attribute__ ((visibility ("hidden"))) void gio_main(void); -__attribute__ ((visibility ("hidden"))) void gio_init(void); -__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans); -__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height); -__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle); - -static void writeClipboard(CFTypeRef str) { - @autoreleasepool { - NSString *s = (__bridge NSString *)str; - NSPasteboard *p = NSPasteboard.generalPasteboard; - [p declareTypes:@[NSPasteboardTypeString] owner:nil]; - [p setString:s forType:NSPasteboardTypeString]; - } -} - -static CFTypeRef readClipboard(void) { - @autoreleasepool { - NSPasteboard *p = NSPasteboard.generalPasteboard; - NSString *content = [p stringForType:NSPasteboardTypeString]; - return (__bridge_retained CFTypeRef)content; - } -} - -static CGFloat viewHeight(CFTypeRef viewRef) { - @autoreleasepool { - NSView *view = (__bridge NSView *)viewRef; - return [view bounds].size.height; - } -} - -static CGFloat viewWidth(CFTypeRef viewRef) { - @autoreleasepool { - NSView *view = (__bridge NSView *)viewRef; - return [view bounds].size.width; - } -} - -static CGFloat getScreenBackingScale(void) { - @autoreleasepool { - return [NSScreen.mainScreen backingScaleFactor]; - } -} - -static CGFloat getViewBackingScale(CFTypeRef viewRef) { - @autoreleasepool { - NSView *view = (__bridge NSView *)viewRef; - return [view.window backingScaleFactor]; - } -} - -static void setNeedsDisplay(CFTypeRef viewRef) { - @autoreleasepool { - NSView *view = (__bridge NSView *)viewRef; - [view setNeedsDisplay:YES]; - } -} - -static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - return [window cascadeTopLeftFromPoint:topLeft]; - } -} - -static void makeKeyAndOrderFront(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - [window makeKeyAndOrderFront:nil]; - } -} - -static void makeFirstResponder(CFTypeRef windowRef, CFTypeRef viewRef) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - NSView *view = (__bridge NSView *)viewRef; - [window makeFirstResponder:view]; - } -} - -static void toggleFullScreen(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - [window toggleFullScreen:nil]; - } -} - -static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - return [window styleMask]; - } -} - -static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - window.styleMask = mask; - } -} - -static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - window.titleVisibility = state; - } -} - -static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - window.titlebarAppearsTransparent = (BOOL)transparent; - } -} - -static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - [window standardWindowButton:btn].hidden = (BOOL)hide; - } -} - -static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - [window performWindowDragWithEvent:(__bridge NSEvent*)evt]; - } -} - -static void closeWindow(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow* window = (__bridge NSWindow *)windowRef; - [window performClose:nil]; - } -} - -static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { - @autoreleasepool { - NSWindow* window = (__bridge NSWindow *)windowRef; - NSSize size = NSMakeSize(width, height); - [window setContentSize:size]; - } -} - -static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { - @autoreleasepool { - NSWindow* window = (__bridge NSWindow *)windowRef; - window.contentMinSize = NSMakeSize(width, height); - } -} - -static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { - @autoreleasepool { - NSWindow* window = (__bridge NSWindow *)windowRef; - window.contentMaxSize = NSMakeSize(width, height); - window.maxFullScreenContentSize = NSMakeSize(width, height); - } -} - -static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) { - @autoreleasepool { - NSWindow* window = (__bridge NSWindow *)windowRef; - NSRect r = NSMakeRect(x, y, w, h); - [window setFrame:r display:YES]; - } -} - -static void resetLayerFrame(CFTypeRef viewRef) { - @autoreleasepool { - NSView* view = (__bridge NSView *)viewRef; - NSRect r = view.frame; - view.layer.frame = r; - } -} - -static void hideWindow(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow* window = (__bridge NSWindow *)windowRef; - [window miniaturize:window]; - } -} - -static void unhideWindow(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow* window = (__bridge NSWindow *)windowRef; - [window deminiaturize:window]; - } -} - -static NSRect getScreenFrame(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow* window = (__bridge NSWindow *)windowRef; - return [[window screen] frame]; - } -} - -static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - window.title = (__bridge NSString *)titleRef; - } -} - -static int isWindowZoomed(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - return window.zoomed ? 1 : 0; - } -} - -static int isWindowMiniaturized(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - return window.miniaturized ? 1 : 0; - } -} - -static void zoomWindow(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - [window zoom:nil]; - } -} - -static CFTypeRef layerForView(CFTypeRef viewRef) { - @autoreleasepool { - NSView *view = (__bridge NSView *)viewRef; - return (__bridge CFTypeRef)view.layer; - } -} - -static CFTypeRef windowForView(CFTypeRef viewRef) { - @autoreleasepool { - NSView *view = (__bridge NSView *)viewRef; - return (__bridge CFTypeRef)view.window; - } -} - -static void raiseWindow(CFTypeRef windowRef) { - @autoreleasepool { - NSRunningApplication *currentApp = [NSRunningApplication currentApplication]; - if (![currentApp isActive]) { - [currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)]; - } - NSWindow* window = (__bridge NSWindow *)windowRef; - [window makeKeyAndOrderFront:nil]; - } -} - -static CFTypeRef createInputContext(CFTypeRef clientRef) { - @autoreleasepool { - id client = (__bridge id)clientRef; - NSTextInputContext *ctx = [[NSTextInputContext alloc] initWithClient:client]; - return CFBridgingRetain(ctx); - } -} - -static void discardMarkedText(CFTypeRef viewRef) { - @autoreleasepool { - id view = (__bridge id)viewRef; - NSTextInputContext *ctx = [NSTextInputContext currentInputContext]; - if (view == [ctx client]) { - [ctx discardMarkedText]; - } - } -} - -static void invalidateCharacterCoordinates(CFTypeRef viewRef) { - @autoreleasepool { - id view = (__bridge id)viewRef; - NSTextInputContext *ctx = [NSTextInputContext currentInputContext]; - if (view == [ctx client]) { - [ctx invalidateCharacterCoordinates]; - } - } -} - -static void interpretKeyEvents(CFTypeRef viewRef, CFTypeRef eventRef) { - @autoreleasepool { - NSView *view = (__bridge NSView *)viewRef; - NSEvent *event = (__bridge NSEvent *)eventRef; - [view interpretKeyEvents:[NSArray arrayWithObject:event]]; - } -} - -static int isMiniaturized(CFTypeRef windowRef) { - @autoreleasepool { - NSWindow *window = (__bridge NSWindow *)windowRef; - return window.miniaturized ? 1 : 0; - } -} -*/ -import "C" - -func init() { - // Darwin requires that UI operations happen on the main thread only. - runtime.LockOSThread() - // Register launch finished listener. - C.gio_init() -} - -// AppKitViewEvent notifies the client of changes to the window AppKit handles. -// The handles are retained until another AppKitViewEvent is sent. -type AppKitViewEvent struct { - // View is a CFTypeRef for the NSView for the window. - View uintptr - // Layer is a CFTypeRef of the CALayer of View. - Layer uintptr -} - -type window struct { - view C.CFTypeRef - w *callbacks - anim bool - displayLink *displayLink - // redraw is a single entry channel for making sure only one - // display link redraw request is in flight. - redraw chan struct{} - cursor pointer.Cursor - pointerBtns pointer.Buttons - loop *eventLoop - lastMods C.NSUInteger - - scale float32 - config Config - - keysDown map[key.Name]struct{} - // cmdKeys is for storing the current key event while - // waiting for a doCommandBySelector. - cmdKeys cmdKeys -} - -type cmdKeys struct { - eventStr string - eventMods key.Modifiers -} - -// launched is closed when applicationDidFinishLaunching is called. -var launched = make(chan struct{}) - -// nextTopLeft is the offset to use for the next window's call to -// cascadeTopLeftFromPoint. -var nextTopLeft C.NSPoint - -func windowFor(h C.uintptr_t) *window { - return cgo.Handle(h).Value().(*window) -} - -func (w *window) contextView() C.CFTypeRef { - return w.view -} - -func (w *window) ReadClipboard() { - cstr := C.readClipboard() - if cstr != 0 { - defer C.CFRelease(cstr) - } - content := nsstringToString(cstr) - w.ProcessEvent(transfer.DataEvent{ - Type: "application/text", - Open: func() io.ReadCloser { - return io.NopCloser(strings.NewReader(content)) - }, - }) -} - -func (w *window) WriteClipboard(mime string, s []byte) { - cstr := stringToNSString(string(s)) - defer C.CFRelease(cstr) - C.writeClipboard(cstr) -} - -func (w *window) updateWindowMode() { - w.scale = float32(C.getViewBackingScale(w.view)) - wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view)) - w.config.Size = image.Point{ - X: int(wf*w.scale + .5), - Y: int(hf*w.scale + .5), - } - w.config.Mode = Windowed - window := C.windowForView(w.view) - if window == 0 { - return - } - style := int(C.getWindowStyleMask(C.windowForView(w.view))) - switch { - case style&C.NSWindowStyleMaskFullScreen != 0: - w.config.Mode = Fullscreen - case C.isWindowZoomed(window) != 0: - w.config.Mode = Maximized - } - w.config.Decorated = style&C.NSWindowStyleMaskFullSizeContentView == 0 -} - -func (w *window) Configure(options []Option) { - screenScale := float32(C.getScreenBackingScale()) - cfg := configFor(screenScale) - cnf := w.config - cnf.apply(cfg, options) - window := C.windowForView(w.view) - - mask := C.getWindowStyleMask(window) - fullscreen := mask&C.NSWindowStyleMaskFullScreen != 0 - switch cnf.Mode { - case Fullscreen: - if C.isWindowMiniaturized(window) != 0 { - C.unhideWindow(window) - } - if !fullscreen { - C.toggleFullScreen(window) - } - case Minimized: - C.hideWindow(window) - case Maximized: - if C.isWindowMiniaturized(window) != 0 { - C.unhideWindow(window) - } - if fullscreen { - C.toggleFullScreen(window) - } - w.setTitle(cnf.Title) - if C.isWindowZoomed(window) == 0 { - C.zoomWindow(window) - } - case Windowed: - if C.isWindowMiniaturized(window) != 0 { - C.unhideWindow(window) - } - if fullscreen { - C.toggleFullScreen(window) - } - w.setTitle(cnf.Title) - w.config.Size = cnf.Size - cnf.Size = cnf.Size.Div(int(screenScale)) - C.setSize(window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y)) - w.config.MinSize = cnf.MinSize - cnf.MinSize = cnf.MinSize.Div(int(screenScale)) - C.setMinSize(window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y)) - w.config.MaxSize = cnf.MaxSize - cnf.MaxSize = cnf.MaxSize.Div(int(screenScale)) - if cnf.MaxSize != (image.Point{}) { - C.setMaxSize(window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y)) - } - if C.isWindowZoomed(window) != 0 { - C.zoomWindow(window) - } - } - style := C.NSWindowStyleMask(C.NSWindowStyleMaskTitled | C.NSWindowStyleMaskResizable | C.NSWindowStyleMaskMiniaturizable | C.NSWindowStyleMaskClosable) - style = C.NSWindowStyleMaskFullSizeContentView - mask &^= style - barTrans := C.int(C.NO) - titleVis := C.NSWindowTitleVisibility(C.NSWindowTitleVisible) - if !cnf.Decorated { - mask |= style - barTrans = C.YES - titleVis = C.NSWindowTitleHidden - } - C.setWindowTitlebarAppearsTransparent(window, barTrans) - C.setWindowTitleVisibility(window, titleVis) - C.setWindowStyleMask(window, mask) - C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans) - C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans) - C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans) - // When toggling the titlebar, the layer doesn't update its frame - // until the next resize. Force it. - C.resetLayerFrame(w.view) -} - -func (w *window) setTitle(title string) { - w.config.Title = title - titleC := stringToNSString(title) - defer C.CFRelease(titleC) - C.setTitle(C.windowForView(w.view), titleC) -} - -func (w *window) Perform(acts system.Action) { - window := C.windowForView(w.view) - walkActions(acts, func(a system.Action) { - switch a { - case system.ActionCenter: - r := C.getScreenFrame(window) // the screen size of the window - screenScale := float32(C.getScreenBackingScale()) - sz := w.config.Size.Div(int(screenScale)) - x := (int(r.size.width) - sz.X) / 2 - y := (int(r.size.height) - sz.Y) / 2 - C.setScreenFrame(window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y)) - case system.ActionRaise: - C.raiseWindow(window) - } - }) - if acts&system.ActionClose != 0 { - C.closeWindow(window) - } -} - -func (w *window) SetCursor(cursor pointer.Cursor) { - w.cursor = windowSetCursor(w.cursor, cursor) -} - -func (w *window) EditorStateChanged(old, new editorState) { - if old.Selection.Range != new.Selection.Range || !areSnippetsConsistent(old.Snippet, new.Snippet) { - C.discardMarkedText(w.view) - w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) - } - if old.Selection.Caret != new.Selection.Caret || old.Selection.Transform != new.Selection.Transform { - C.invalidateCharacterCoordinates(w.view) - } -} - -func (w *window) ShowTextInput(show bool) {} - -func (w *window) SetInputHint(_ key.InputHint) {} - -func (w *window) SetAnimating(anim bool) { - w.anim = anim - window := C.windowForView(w.view) - if w.anim && window != 0 && C.isMiniaturized(window) == 0 { - w.displayLink.Start() - } else { - w.displayLink.Stop() - } -} - -func (w *window) runOnMain(f func()) { - runOnMain(func() { - // Make sure the view is still valid. The window might've been closed - // during the switch to the main thread. - if w.view != 0 { - f() - } - }) -} - -//export gio_onKeys -func gio_onKeys(h C.uintptr_t, event C.CFTypeRef, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) { - w := windowFor(h) - if w.keysDown == nil { - w.keysDown = make(map[key.Name]struct{}) - } - str := nsstringToString(cstr) - kmods := convertMods(mods) - ks := key.Release - if keyDown { - ks = key.Press - w.cmdKeys.eventStr = str - w.cmdKeys.eventMods = kmods - C.interpretKeyEvents(w.view, event) - } - for _, k := range str { - if n, ok := convertKey(k); ok { - ke := key.Event{ - Name: n, - Modifiers: kmods, - State: ks, - } - if keyDown { - w.keysDown[ke.Name] = struct{}{} - if _, isCmd := convertCommandKey(k); isCmd || kmods.Contain(key.ModCommand) { - // doCommandBySelector already processed the event. - return - } - } else { - if _, pressed := w.keysDown[n]; !pressed { - continue - } - delete(w.keysDown, n) - } - w.ProcessEvent(ke) - } - } -} - -//export gio_onCommandBySelector -func gio_onCommandBySelector(h C.uintptr_t) C.bool { - w := windowFor(h) - ev := w.cmdKeys - w.cmdKeys = cmdKeys{} - handled := false - for _, k := range ev.eventStr { - n, ok := convertCommandKey(k) - if !ok && ev.eventMods.Contain(key.ModCommand) { - n, ok = convertKey(k) - } - if !ok { - continue - } - ke := key.Event{ - Name: n, - Modifiers: ev.eventMods, - State: key.Press, - } - handled = w.processEvent(ke) || handled - } - return C.bool(handled) -} - -//export gio_onFlagsChanged -func gio_onFlagsChanged(h C.uintptr_t, curMods C.NSUInteger) { - w := windowFor(h) - - mods := []C.NSUInteger{C.NSControlKeyMask, C.NSAlternateKeyMask, C.NSShiftKeyMask, C.NSCommandKeyMask} - keys := []key.Name{key.NameCtrl, key.NameAlt, key.NameShift, key.NameCommand} - - for i, mod := range mods { - wasPressed := w.lastMods&mod != 0 - isPressed := curMods&mod != 0 - - if wasPressed != isPressed { - st := key.Release - if isPressed { - st = key.Press - } - w.ProcessEvent(key.Event{ - Name: keys[i], - State: st, - }) - } - } - - w.lastMods = curMods -} - -//export gio_onText -func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) { - str := nsstringToString(cstr) - w := windowFor(h) - w.w.EditorInsert(str) -} - -//export gio_onMouse -func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) { - w := windowFor(h) - t := time.Duration(float64(ti)*float64(time.Second) + .5) - xf, yf := float32(x)*w.scale, float32(y)*w.scale - dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale - pos := f32.Point{X: xf, Y: yf} - var btn pointer.Buttons - switch cbtn { - case 0: - btn = pointer.ButtonPrimary - case 1: - btn = pointer.ButtonSecondary - case 2: - btn = pointer.ButtonTertiary - } - var typ pointer.Kind - switch cdir { - case C.MOUSE_MOVE: - typ = pointer.Move - case C.MOUSE_UP: - typ = pointer.Release - w.pointerBtns &^= btn - case C.MOUSE_DOWN: - typ = pointer.Press - w.pointerBtns |= btn - act, ok := w.w.ActionAt(pos) - if ok && w.config.Mode != Fullscreen { - switch act { - case system.ActionMove: - C.performWindowDragWithEvent(C.windowForView(w.view), evt) - return - } - } - case C.MOUSE_SCROLL: - typ = pointer.Scroll - default: - panic("invalid direction") - } - w.ProcessEvent(pointer.Event{ - Kind: typ, - Source: pointer.Mouse, - Time: t, - Buttons: w.pointerBtns, - Position: pos, - Scroll: f32.Point{X: dxf, Y: dyf}, - Modifiers: convertMods(mods), - }) -} - -//export gio_onDraw -func gio_onDraw(h C.uintptr_t) { - w := windowFor(h) - w.draw() -} - -//export gio_onFocus -func gio_onFocus(h C.uintptr_t, focus C.int) { - w := windowFor(h) - w.SetCursor(w.cursor) - w.config.Focused = focus == 1 - w.ProcessEvent(ConfigEvent{Config: w.config}) -} - -//export gio_onChangeScreen -func gio_onChangeScreen(h C.uintptr_t, did uint64) { - w := windowFor(h) - w.displayLink.SetDisplayID(did) - C.setNeedsDisplay(w.view) -} - -//export gio_hasMarkedText -func gio_hasMarkedText(h C.uintptr_t) C.int { - w := windowFor(h) - state := w.w.EditorState() - if state.compose.Start != -1 { - return 1 - } - return 0 -} - -//export gio_markedRange -func gio_markedRange(h C.uintptr_t) C.NSRange { - w := windowFor(h) - state := w.w.EditorState() - rng := state.compose - start, end := rng.Start, rng.End - if start == -1 { - return C.NSMakeRange(C.NSNotFound, 0) - } - u16start := state.UTF16Index(start) - return C.NSMakeRange( - C.NSUInteger(u16start), - C.NSUInteger(state.UTF16Index(end)-u16start), - ) -} - -//export gio_selectedRange -func gio_selectedRange(h C.uintptr_t) C.NSRange { - w := windowFor(h) - state := w.w.EditorState() - rng := state.Selection - start, end := rng.Start, rng.End - if start > end { - start, end = end, start - } - u16start := state.UTF16Index(start) - return C.NSMakeRange( - C.NSUInteger(u16start), - C.NSUInteger(state.UTF16Index(end)-u16start), - ) -} - -//export gio_unmarkText -func gio_unmarkText(h C.uintptr_t) { - w := windowFor(h) - w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) -} - -//export gio_setMarkedText -func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) { - w := windowFor(h) - str := nsstringToString(cstr) - state := w.w.EditorState() - rng := state.compose - if rng.Start == -1 { - rng = state.Selection.Range - } - if replaceRange.location != C.NSNotFound { - // replaceRange is relative to marked (or selected) text. - offset := state.UTF16Index(rng.Start) - start := state.RunesIndex(int(replaceRange.location) + offset) - end := state.RunesIndex(int(replaceRange.location+replaceRange.length) + offset) - rng = key.Range{ - Start: start, - End: end, - } - } - w.w.EditorReplace(rng, str) - comp := key.Range{ - Start: rng.Start, - End: rng.Start + utf8.RuneCountInString(str), - } - w.w.SetComposingRegion(comp) - - sel := key.Range{Start: comp.End, End: comp.End} - if selRange.location != C.NSNotFound { - // selRange is relative to inserted text. - offset := state.UTF16Index(rng.Start) - start := state.RunesIndex(int(selRange.location) + offset) - end := state.RunesIndex(int(selRange.location+selRange.length) + offset) - sel = key.Range{ - Start: start, - End: end, - } - } - w.w.SetEditorSelection(sel) -} - -//export gio_substringForProposedRange -func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef { - w := windowFor(h) - state := w.w.EditorState() - start, end := state.Snippet.Start, state.Snippet.End - if start > end { - start, end = end, start - } - rng := key.Range{ - Start: state.RunesIndex(int(crng.location)), - End: state.RunesIndex(int(crng.location + crng.length)), - } - if rng.Start < start || end < rng.End { - w.w.SetEditorSnippet(rng) - } - u16start := state.UTF16Index(start) - actual.location = C.NSUInteger(u16start) - actual.length = C.NSUInteger(state.UTF16Index(end) - u16start) - return stringToNSString(state.Snippet.Text) -} - -//export gio_insertText -func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) { - w := windowFor(h) - str := nsstringToString(cstr) - // macOS IME in some cases calls insertText for command keys such as backspace - // instead of doCommandBySelector. - for _, r := range str { - if _, ok := convertCommandKey(r); ok { - w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) - return - } - } - state := w.w.EditorState() - rng := state.compose - if rng.Start == -1 { - rng = state.Selection.Range - } - if crng.location != C.NSNotFound { - rng = key.Range{ - Start: state.RunesIndex(int(crng.location)), - End: state.RunesIndex(int(crng.location + crng.length)), - } - } - w.w.EditorReplace(rng, str) - w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) - start := rng.Start - if rng.End < start { - start = rng.End - } - pos := start + utf8.RuneCountInString(str) - w.w.SetEditorSelection(key.Range{Start: pos, End: pos}) -} - -//export gio_characterIndexForPoint -func gio_characterIndexForPoint(h C.uintptr_t, p C.NSPoint) C.NSUInteger { - return C.NSNotFound -} - -//export gio_firstRectForCharacterRange -func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.NSRect { - w := windowFor(h) - state := w.w.EditorState() - sel := state.Selection - u16start := state.UTF16Index(sel.Start) - actual.location = C.NSUInteger(u16start) - actual.length = 0 - // Transform to NSView local coordinates (lower left origin, undo backing scale). - scale := 1. / float32(C.getViewBackingScale(w.view)) - height := float32(C.viewHeight(w.view)) - local := f32.AffineId().Scale(f32.Pt(0, 0), f32.Pt(scale, -scale)).Offset(f32.Pt(0, height)) - t := local.Mul(sel.Transform) - bounds := f32.Rectangle{ - Min: t.Transform(sel.Pos.Sub(f32.Pt(0, sel.Ascent))), - Max: t.Transform(sel.Pos.Add(f32.Pt(0, sel.Descent))), - }.Canon() - sz := bounds.Size() - return C.NSMakeRect( - C.CGFloat(bounds.Min.X), C.CGFloat(bounds.Min.Y), - C.CGFloat(sz.X), C.CGFloat(sz.Y), - ) -} - -func (w *window) draw() { - cnf := w.config - w.updateWindowMode() - if w.config != cnf { - w.ProcessEvent(ConfigEvent{Config: w.config}) - } - select { - case <-w.redraw: - default: - } - if w.anim { - w.SetAnimating(w.anim) - } - sz := w.config.Size - if sz.X == 0 || sz.Y == 0 { - return - } - cfg := configFor(w.scale) - w.ProcessEvent(frameEvent{ - FrameEvent: FrameEvent{ - Now: time.Now(), - Size: sz, - Metric: cfg, - }, - Sync: true, - }) -} - -func (w *window) ProcessEvent(e event.Event) { - w.processEvent(e) -} - -func (w *window) processEvent(e event.Event) bool { - handled := w.w.ProcessEvent(e) - w.loop.FlushEvents() - return handled -} - -func (w *window) Event() event.Event { - return w.loop.Event() -} - -func (w *window) Invalidate() { - w.loop.Invalidate() -} - -func (w *window) Run(f func()) { - w.loop.Run(f) -} - -func (w *window) Frame(frame *op.Ops) { - w.loop.Frame(frame) -} - -func configFor(scale float32) unit.Metric { - return unit.Metric{ - PxPerDp: scale, - PxPerSp: scale, - } -} - -//export gio_onAttached -func gio_onAttached(h C.uintptr_t, attached C.int) { - w := windowFor(h) - if attached != 0 { - layer := C.layerForView(w.view) - w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)}) - } else { - w.ProcessEvent(AppKitViewEvent{}) - w.SetAnimating(w.anim) - } -} - -//export gio_onDestroy -func gio_onDestroy(h C.uintptr_t) { - w := windowFor(h) - w.ProcessEvent(DestroyEvent{}) - w.displayLink.Close() - w.displayLink = nil - cgo.Handle(h).Delete() - w.view = 0 -} - -//export gio_onFinishLaunching -func gio_onFinishLaunching() { - close(launched) -} - -func newWindow(win *callbacks, options []Option) { - <-launched - res := make(chan struct{}) - runOnMain(func() { - w := &window{ - redraw: make(chan struct{}, 1), - w: win, - } - w.loop = newEventLoop(w.w, w.wakeup) - win.SetDriver(w) - res <- struct{}{} - var cnf Config - cnf.apply(unit.Metric{}, options) - if err := w.init(cnf.CustomRenderer); err != nil { - w.ProcessEvent(DestroyEvent{Err: err}) - return - } - window := C.gio_createWindow(w.view, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y)) - // Release our reference now that the NSWindow has it. - C.CFRelease(w.view) - w.Configure(options) - if nextTopLeft.x == 0 && nextTopLeft.y == 0 { - // cascadeTopLeftFromPoint treats (0, 0) as a no-op, - // and just returns the offset we need for the first window. - nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft) - } - nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft) - C.makeFirstResponder(window, w.view) - // makeKeyAndOrderFront assumes ownership of our window reference. - C.makeKeyAndOrderFront(window) - }) - <-res -} - -func (w *window) init(customRenderer bool) error { - presentWithTrans := 1 - if customRenderer { - presentWithTrans = 0 - } - view := C.gio_createView(C.int(presentWithTrans)) - if view == 0 { - return errors.New("newOSWindow: failed to create view") - } - scale := float32(C.getViewBackingScale(view)) - w.scale = scale - dl, err := newDisplayLink(func() { - select { - case w.redraw <- struct{}{}: - default: - return - } - w.runOnMain(func() { - C.setNeedsDisplay(w.view) - }) - }) - w.displayLink = dl - if err != nil { - C.CFRelease(view) - return err - } - C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w))) - w.view = view - return nil -} - -func osMain() { - if !isMainThread() { - panic("app.Main must run on the main goroutine") - } - C.gio_main() -} - -func convertCommandKey(k rune) (key.Name, bool) { - var n key.Name - switch k { - case '\x1b': // ASCII escape. - n = key.NameEscape - case C.NSLeftArrowFunctionKey: - n = key.NameLeftArrow - case C.NSRightArrowFunctionKey: - n = key.NameRightArrow - case C.NSUpArrowFunctionKey: - n = key.NameUpArrow - case C.NSDownArrowFunctionKey: - n = key.NameDownArrow - case '\r': - n = key.NameReturn - case '\x03': - n = key.NameEnter - case C.NSHomeFunctionKey: - n = key.NameHome - case C.NSEndFunctionKey: - n = key.NameEnd - case '\x7f', '\b': - n = key.NameDeleteBackward - case C.NSDeleteFunctionKey: - n = key.NameDeleteForward - case '\t', 0x19: - n = key.NameTab - case C.NSPageUpFunctionKey: - n = key.NamePageUp - case C.NSPageDownFunctionKey: - n = key.NamePageDown - default: - return "", false - } - return n, true -} - -func convertKey(k rune) (key.Name, bool) { - if n, ok := convertCommandKey(k); ok { - return n, true - } - var n key.Name - switch k { - case C.NSF1FunctionKey: - n = key.NameF1 - case C.NSF2FunctionKey: - n = key.NameF2 - case C.NSF3FunctionKey: - n = key.NameF3 - case C.NSF4FunctionKey: - n = key.NameF4 - case C.NSF5FunctionKey: - n = key.NameF5 - case C.NSF6FunctionKey: - n = key.NameF6 - case C.NSF7FunctionKey: - n = key.NameF7 - case C.NSF8FunctionKey: - n = key.NameF8 - case C.NSF9FunctionKey: - n = key.NameF9 - case C.NSF10FunctionKey: - n = key.NameF10 - case C.NSF11FunctionKey: - n = key.NameF11 - case C.NSF12FunctionKey: - n = key.NameF12 - case 0x20: - n = key.NameSpace - default: - k = unicode.ToUpper(k) - if !unicode.IsPrint(k) { - return "", false - } - n = key.Name(k) - } - return n, true -} - -func convertMods(mods C.NSUInteger) key.Modifiers { - var kmods key.Modifiers - if mods&C.NSAlternateKeyMask != 0 { - kmods |= key.ModAlt - } - if mods&C.NSControlKeyMask != 0 { - kmods |= key.ModCtrl - } - if mods&C.NSCommandKeyMask != 0 { - kmods |= key.ModCommand - } - if mods&C.NSShiftKeyMask != 0 { - kmods |= key.ModShift - } - return kmods -} - -func (AppKitViewEvent) implementsViewEvent() {} -func (AppKitViewEvent) ImplementsEvent() {} -func (a AppKitViewEvent) Valid() bool { - return a != (AppKitViewEvent{}) -} diff --git a/gio/app/os_unix.go b/gio/app/os_unix.go deleted file mode 100644 index a1f33dd..0000000 --- a/gio/app/os_unix.go +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build (linux && !android) || freebsd || openbsd -// +build linux,!android freebsd openbsd - -package app - -import ( - "errors" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/io/pointer" -) - -type X11ViewEvent struct { - // Display is a pointer to the X11 Display created by XOpenDisplay. - Display unsafe.Pointer - // Window is the X11 window ID as returned by XCreateWindow. - Window uintptr -} - -func (X11ViewEvent) implementsViewEvent() {} -func (X11ViewEvent) ImplementsEvent() {} -func (x X11ViewEvent) Valid() bool { - return x != (X11ViewEvent{}) -} - -type WaylandViewEvent struct { - // Display is the *wl_display returned by wl_display_connect. - Display unsafe.Pointer - // Surface is the *wl_surface returned by wl_compositor_create_surface. - Surface unsafe.Pointer -} - -func (WaylandViewEvent) implementsViewEvent() {} -func (WaylandViewEvent) ImplementsEvent() {} -func (w WaylandViewEvent) Valid() bool { - return w != (WaylandViewEvent{}) -} - -func osMain() { - select {} -} - -type windowDriver func(*callbacks, []Option) error - -// Instead of creating files with build tags for each combination of wayland +/- x11 -// let each driver initialize these variables with their own version of createWindow. -var wlDriver, x11Driver windowDriver - -func newWindow(window *callbacks, options []Option) { - var errFirst error - for _, d := range []windowDriver{wlDriver, x11Driver} { - if d == nil { - continue - } - err := d(window, options) - if err == nil { - return - } - if errFirst == nil { - errFirst = err - } - } - if errFirst == nil { - errFirst = errors.New("app: no window driver available") - } - window.ProcessEvent(DestroyEvent{Err: errFirst}) -} - -// xCursor contains mapping from pointer.Cursor to XCursor. -var xCursor = [...]string{ - pointer.CursorDefault: "left_ptr", - pointer.CursorNone: "", - pointer.CursorText: "xterm", - pointer.CursorVerticalText: "vertical-text", - pointer.CursorPointer: "hand2", - pointer.CursorCrosshair: "crosshair", - pointer.CursorAllScroll: "fleur", - pointer.CursorColResize: "sb_h_double_arrow", - pointer.CursorRowResize: "sb_v_double_arrow", - pointer.CursorGrab: "hand1", - pointer.CursorGrabbing: "move", - pointer.CursorNotAllowed: "crossed_circle", - pointer.CursorWait: "watch", - pointer.CursorProgress: "left_ptr_watch", - pointer.CursorNorthWestResize: "top_left_corner", - pointer.CursorNorthEastResize: "top_right_corner", - pointer.CursorSouthWestResize: "bottom_left_corner", - pointer.CursorSouthEastResize: "bottom_right_corner", - pointer.CursorNorthSouthResize: "sb_v_double_arrow", - pointer.CursorEastWestResize: "sb_h_double_arrow", - pointer.CursorWestResize: "left_side", - pointer.CursorEastResize: "right_side", - pointer.CursorNorthResize: "top_side", - pointer.CursorSouthResize: "bottom_side", - pointer.CursorNorthEastSouthWestResize: "fd_double_arrow", - pointer.CursorNorthWestSouthEastResize: "bd_double_arrow", -} diff --git a/gio/app/os_wayland.c b/gio/app/os_wayland.c deleted file mode 100644 index a9752aa..0000000 --- a/gio/app/os_wayland.c +++ /dev/null @@ -1,124 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build ((linux && !android) || freebsd) && !nowayland -// +build linux,!android freebsd -// +build !nowayland - -#include -#include "wayland_xdg_shell.h" -#include "wayland_xdg_decoration.h" -#include "wayland_text_input.h" -#include "_cgo_export.h" - -const struct wl_registry_listener gio_registry_listener = { - // Cast away const parameter. - .global = (void (*)(void *, struct wl_registry *, uint32_t, const char *, uint32_t))gio_onRegistryGlobal, - .global_remove = gio_onRegistryGlobalRemove -}; - -const struct wl_surface_listener gio_surface_listener = { - .enter = gio_onSurfaceEnter, - .leave = gio_onSurfaceLeave, -}; - -const struct xdg_surface_listener gio_xdg_surface_listener = { - .configure = gio_onXdgSurfaceConfigure, -}; - -const struct xdg_toplevel_listener gio_xdg_toplevel_listener = { - .configure = gio_onToplevelConfigure, - .close = gio_onToplevelClose, -}; - -const struct zxdg_toplevel_decoration_v1_listener gio_zxdg_toplevel_decoration_v1_listener = { - .configure = gio_onToplevelDecorationConfigure, -}; - -static void xdg_wm_base_handle_ping(void *data, struct xdg_wm_base *wm, uint32_t serial) { - xdg_wm_base_pong(wm, serial); -} - -const struct xdg_wm_base_listener gio_xdg_wm_base_listener = { - .ping = xdg_wm_base_handle_ping, -}; - -const struct wl_callback_listener gio_callback_listener = { - .done = gio_onFrameDone, -}; - -const struct wl_output_listener gio_output_listener = { - // Cast away const parameter. - .geometry = (void (*)(void *, struct wl_output *, int32_t, int32_t, int32_t, int32_t, int32_t, const char *, const char *, int32_t))gio_onOutputGeometry, - .mode = gio_onOutputMode, - .done = gio_onOutputDone, - .scale = gio_onOutputScale, -}; - -const struct wl_seat_listener gio_seat_listener = { - .capabilities = gio_onSeatCapabilities, - // Cast away const parameter. - .name = (void (*)(void *, struct wl_seat *, const char *))gio_onSeatName, -}; - -const struct wl_pointer_listener gio_pointer_listener = { - .enter = gio_onPointerEnter, - .leave = gio_onPointerLeave, - .motion = gio_onPointerMotion, - .button = gio_onPointerButton, - .axis = gio_onPointerAxis, - .frame = gio_onPointerFrame, - .axis_source = gio_onPointerAxisSource, - .axis_stop = gio_onPointerAxisStop, - .axis_discrete = gio_onPointerAxisDiscrete, -}; - -const struct wl_touch_listener gio_touch_listener = { - .down = gio_onTouchDown, - .up = gio_onTouchUp, - .motion = gio_onTouchMotion, - .frame = gio_onTouchFrame, - .cancel = gio_onTouchCancel, -}; - -const struct wl_keyboard_listener gio_keyboard_listener = { - .keymap = gio_onKeyboardKeymap, - .enter = gio_onKeyboardEnter, - .leave = gio_onKeyboardLeave, - .key = gio_onKeyboardKey, - .modifiers = gio_onKeyboardModifiers, - .repeat_info = gio_onKeyboardRepeatInfo -}; - -const struct zwp_text_input_v3_listener gio_zwp_text_input_v3_listener = { - .enter = gio_onTextInputEnter, - .leave = gio_onTextInputLeave, - // Cast away const parameter. - .preedit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *, int32_t, int32_t))gio_onTextInputPreeditString, - .commit_string = (void (*)(void *, struct zwp_text_input_v3 *, const char *))gio_onTextInputCommitString, - .delete_surrounding_text = gio_onTextInputDeleteSurroundingText, - .done = gio_onTextInputDone -}; - -const struct wl_data_device_listener gio_data_device_listener = { - .data_offer = gio_onDataDeviceOffer, - .enter = gio_onDataDeviceEnter, - .leave = gio_onDataDeviceLeave, - .motion = gio_onDataDeviceMotion, - .drop = gio_onDataDeviceDrop, - .selection = gio_onDataDeviceSelection, -}; - -const struct wl_data_offer_listener gio_data_offer_listener = { - .offer = (void (*)(void *, struct wl_data_offer *, const char *))gio_onDataOfferOffer, - .source_actions = gio_onDataOfferSourceActions, - .action = gio_onDataOfferAction, -}; - -const struct wl_data_source_listener gio_data_source_listener = { - .target = (void (*)(void *, struct wl_data_source *, const char *))gio_onDataSourceTarget, - .send = (void (*)(void *, struct wl_data_source *, const char *, int32_t))gio_onDataSourceSend, - .cancelled = gio_onDataSourceCancelled, - .dnd_drop_performed = gio_onDataSourceDNDDropPerformed, - .dnd_finished = gio_onDataSourceDNDFinished, - .action = gio_onDataSourceAction, -}; diff --git a/gio/app/os_wayland.go b/gio/app/os_wayland.go deleted file mode 100644 index 179643f..0000000 --- a/gio/app/os_wayland.go +++ /dev/null @@ -1,1945 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build ((linux && !android) || freebsd) && !nowayland -// +build linux,!android freebsd -// +build !nowayland - -package app - -import ( - "bytes" - "errors" - "fmt" - "image" - "io" - "math" - "os" - "os/exec" - "runtime" - "strconv" - "sync" - "time" - "unsafe" - - syscall "golang.org/x/sys/unix" - - "github.com/p9c/p9/pkg/gel/gio/app/internal/xkb" - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/fling" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// Use wayland-scanner to generate glue code for the xdg-shell and xdg-decoration extensions. -//go:generate wayland-scanner client-header /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.h -//go:generate wayland-scanner private-code /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml wayland_xdg_shell.c - -//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.h -//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/text-input/text-input-unstable-v3.xml wayland_text_input.c - -//go:generate wayland-scanner client-header /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.h -//go:generate wayland-scanner private-code /usr/share/wayland-protocols/unstable/xdg-decoration/xdg-decoration-unstable-v1.xml wayland_xdg_decoration.c - -//go:generate sed -i "1s;^;//go:build ((linux \\&\\& !android) || freebsd) \\&\\& !nowayland\\n// +build linux,!android freebsd\\n// +build !nowayland\\n\\n;" wayland_xdg_shell.c -//go:generate sed -i "1s;^;//go:build ((linux \\&\\& !android) || freebsd) \\&\\& !nowayland\\n// +build linux,!android freebsd\\n// +build !nowayland\\n\\n;" wayland_xdg_decoration.c -//go:generate sed -i "1s;^;//go:build ((linux \\&\\& !android) || freebsd) \\&\\& !nowayland\\n// +build linux,!android freebsd\\n// +build !nowayland\\n\\n;" wayland_text_input.c - -/* -#cgo linux pkg-config: wayland-client wayland-cursor -#cgo freebsd openbsd LDFLAGS: -lwayland-client -lwayland-cursor -#cgo freebsd CFLAGS: -I/usr/local/include -#cgo freebsd LDFLAGS: -L/usr/local/lib - -#include -#include -#include -#include "wayland_text_input.h" -#include "wayland_xdg_shell.h" -#include "wayland_xdg_decoration.h" - -extern const struct wl_registry_listener gio_registry_listener; -extern const struct wl_surface_listener gio_surface_listener; -extern const struct xdg_surface_listener gio_xdg_surface_listener; -extern const struct xdg_toplevel_listener gio_xdg_toplevel_listener; -extern const struct zxdg_toplevel_decoration_v1_listener gio_zxdg_toplevel_decoration_v1_listener; -extern const struct xdg_wm_base_listener gio_xdg_wm_base_listener; -extern const struct wl_callback_listener gio_callback_listener; -extern const struct wl_output_listener gio_output_listener; -extern const struct wl_seat_listener gio_seat_listener; -extern const struct wl_pointer_listener gio_pointer_listener; -extern const struct wl_touch_listener gio_touch_listener; -extern const struct wl_keyboard_listener gio_keyboard_listener; -extern const struct zwp_text_input_v3_listener gio_zwp_text_input_v3_listener; -extern const struct wl_data_device_listener gio_data_device_listener; -extern const struct wl_data_offer_listener gio_data_offer_listener; -extern const struct wl_data_source_listener gio_data_source_listener; -*/ -import "C" - -type wlDisplay struct { - disp *C.struct_wl_display - reg *C.struct_wl_registry - compositor *C.struct_wl_compositor - wm *C.struct_xdg_wm_base - imm *C.struct_zwp_text_input_manager_v3 - shm *C.struct_wl_shm - dataDeviceManager *C.struct_wl_data_device_manager - decor *C.struct_zxdg_decoration_manager_v1 - seat *wlSeat - xkb *xkb.Context - outputMap map[C.uint32_t]*C.struct_wl_output - outputConfig map[*C.struct_wl_output]*wlOutput - - // Notification pipe fds. - notify struct { - read, write int - } - - repeat repeatState - poller poller - readClipClose chan struct{} -} - -type wlSeat struct { - disp *wlDisplay - seat *C.struct_wl_seat - name C.uint32_t - pointer *C.struct_wl_pointer - touch *C.struct_wl_touch - keyboard *C.struct_wl_keyboard - im *C.struct_zwp_text_input_v3 - - // The most recent input serial. - serial C.uint32_t - // The most recent pointer enter serial. - pointerSerial C.uint32_t - - pointerFocus *window - keyboardFocus *window - touchFoci map[C.int32_t]*window - - // Clipboard support. - dataDev *C.struct_wl_data_device - // offers is a map from active wl_data_offers to - // the list of mime types they support. - offers map[*C.struct_wl_data_offer][]string - // clipboard is the wl_data_offer for the clipboard. - clipboard *C.struct_wl_data_offer - // mimeType is the chosen mime type of clipboard. - mimeType string - // source represents the clipboard content of the most recent - // clipboard write, if any. - source *C.struct_wl_data_source - // content is the data belonging to source. - content []byte -} - -type repeatState struct { - rate int - delay time.Duration - - key uint32 - win *window - stopC chan struct{} - - start time.Duration - last time.Duration - mu sync.Mutex - now time.Duration -} - -type window struct { - w *callbacks - disp *wlDisplay - surf *C.struct_wl_surface - wmSurf *C.struct_xdg_surface - topLvl *C.struct_xdg_toplevel - decor *C.struct_zxdg_toplevel_decoration_v1 - ppdp, ppsp float32 - scroll struct { - time time.Duration - steps image.Point - dist f32.Point - } - pointerBtns pointer.Buttons - lastPos f32.Point - lastTouch f32.Point - - cursor struct { - theme *C.struct_wl_cursor_theme - cursor *C.struct_wl_cursor - // system is the active cursor for system gestures - // such as border resizes and window moves. It - // is nil if the pointer is not in a system gesture - // area. - system *C.struct_wl_cursor - surf *C.struct_wl_surface - cursors struct { - pointer *C.struct_wl_cursor - resizeNorth *C.struct_wl_cursor - resizeSouth *C.struct_wl_cursor - resizeWest *C.struct_wl_cursor - resizeEast *C.struct_wl_cursor - resizeNorthWest *C.struct_wl_cursor - resizeNorthEast *C.struct_wl_cursor - resizeSouthWest *C.struct_wl_cursor - resizeSouthEast *C.struct_wl_cursor - } - } - - fling struct { - yExtrapolation fling.Extrapolation - xExtrapolation fling.Extrapolation - anim fling.Animation - start bool - dir f32.Point - } - - configured bool - lastFrameCallback *C.struct_wl_callback - - animating bool - // The most recent configure serial waiting to be ack'ed. - serial C.uint32_t - scale int - // size is the unscaled window size (unlike config.Size which is scaled). - size image.Point - config Config - wsize image.Point // window config size before going fullscreen or maximized - inCompositor bool // window is moving or being resized - - clipReads chan transfer.DataEvent - - wakeups chan struct{} - - closing bool -} - -type poller struct { - pollfds [2]syscall.PollFd - // buf is scratch space for draining the notification pipe. - buf [100]byte -} - -type wlOutput struct { - width int - height int - physWidth int - physHeight int - transform C.int32_t - scale int - windows []*window -} - -// callbackMap maps Wayland native handles to corresponding Go -// references. It is necessary because the Wayland client API -// forces the use of callbacks and storing pointers to Go values -// in C is forbidden. -var callbackMap sync.Map - -// clipboardMimeTypes is a list of supported clipboard mime types, in -// order of preference. -var clipboardMimeTypes = []string{"text/plain;charset=utf8", "UTF8_STRING", "text/plain", "TEXT", "STRING"} - -var ( - newWaylandEGLContext func(w *window) (context, error) - newWaylandVulkanContext func(w *window) (context, error) -) - -func init() { - wlDriver = newWLWindow -} - -func newWLWindow(callbacks *callbacks, options []Option) error { - d, err := newWLDisplay() - if err != nil { - return err - } - w, err := d.createNativeWindow(options) - if err != nil { - d.destroy() - return err - } - w.w = callbacks - w.w.SetDriver(w) - - // Finish and commit setup from createNativeWindow. - w.Configure(options) - w.draw(true) - C.wl_surface_commit(w.surf) - - w.ProcessEvent(WaylandViewEvent{ - Display: unsafe.Pointer(w.display()), - Surface: unsafe.Pointer(w.surf), - }) - return nil -} - -func (d *wlDisplay) writeClipboard(content []byte) error { - s := d.seat - if s == nil { - return nil - } - // Clear old offer. - if s.source != nil { - C.wl_data_source_destroy(s.source) - s.source = nil - s.content = nil - } - if d.dataDeviceManager == nil || s.dataDev == nil { - return nil - } - s.content = content - s.source = C.wl_data_device_manager_create_data_source(d.dataDeviceManager) - C.wl_data_source_add_listener(s.source, &C.gio_data_source_listener, unsafe.Pointer(s.seat)) - for _, mime := range clipboardMimeTypes { - C.wl_data_source_offer(s.source, C.CString(mime)) - } - C.wl_data_device_set_selection(s.dataDev, s.source, s.serial) - return nil -} - -func (d *wlDisplay) readClipboard() (io.ReadCloser, error) { - s := d.seat - if s == nil { - return nil, nil - } - if s.clipboard == nil { - return nil, nil - } - r, w, err := os.Pipe() - if err != nil { - return nil, err - } - // wl_data_offer_receive performs and implicit dup(2) of the write end - // of the pipe. Close our version. - defer w.Close() - cmimeType := C.CString(s.mimeType) - defer C.free(unsafe.Pointer(cmimeType)) - C.wl_data_offer_receive(s.clipboard, cmimeType, C.int(w.Fd())) - return r, nil -} - -func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) { - if d.compositor == nil { - return nil, errors.New("wayland: no compositor available") - } - if d.wm == nil { - return nil, errors.New("wayland: no xdg_wm_base available") - } - if d.shm == nil { - return nil, errors.New("wayland: no wl_shm available") - } - if len(d.outputMap) == 0 { - return nil, errors.New("wayland: no outputs available") - } - var scale int - for _, conf := range d.outputConfig { - if s := conf.scale; s > scale { - scale = s - } - } - ppdp := detectUIScale() - - w := &window{ - disp: d, - scale: scale, - ppdp: ppdp, - ppsp: ppdp, - wakeups: make(chan struct{}, 1), - clipReads: make(chan transfer.DataEvent, 1), - } - w.surf = C.wl_compositor_create_surface(d.compositor) - if w.surf == nil { - w.destroy() - return nil, errors.New("wayland: wl_compositor_create_surface failed") - } - C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale)) - callbackStore(unsafe.Pointer(w.surf), w) - w.wmSurf = C.xdg_wm_base_get_xdg_surface(d.wm, w.surf) - if w.wmSurf == nil { - w.destroy() - return nil, errors.New("wayland: xdg_wm_base_get_xdg_surface failed") - } - w.topLvl = C.xdg_surface_get_toplevel(w.wmSurf) - if w.topLvl == nil { - w.destroy() - return nil, errors.New("wayland: xdg_surface_get_toplevel failed") - } - - id := C.CString(ID) - defer C.free(unsafe.Pointer(id)) - C.xdg_toplevel_set_app_id(w.topLvl, id) - - cursorTheme := C.CString(os.Getenv("XCURSOR_THEME")) - defer C.free(unsafe.Pointer(cursorTheme)) - cursorSize := 32 - if envSize, ok := os.LookupEnv("XCURSOR_SIZE"); ok && envSize != "" { - size, err := strconv.Atoi(envSize) - if err == nil { - cursorSize = size - } - } - - w.cursor.theme = C.wl_cursor_theme_load(cursorTheme, C.int(cursorSize*w.scale), d.shm) - if w.cursor.theme == nil { - w.destroy() - return nil, errors.New("wayland: wl_cursor_theme_load failed") - } - w.loadCursors() - w.cursor.cursor = w.cursor.cursors.pointer - if w.cursor.cursor == nil { - w.destroy() - return nil, errors.New("wayland: wl_cursor_theme_get_cursor failed") - } - w.cursor.surf = C.wl_compositor_create_surface(d.compositor) - if w.cursor.surf == nil { - w.destroy() - return nil, errors.New("wayland: wl_compositor_create_surface failed") - } - C.wl_surface_set_buffer_scale(w.cursor.surf, C.int32_t(w.scale)) - C.xdg_wm_base_add_listener(d.wm, &C.gio_xdg_wm_base_listener, unsafe.Pointer(w.surf)) - C.wl_surface_add_listener(w.surf, &C.gio_surface_listener, unsafe.Pointer(w.surf)) - C.xdg_surface_add_listener(w.wmSurf, &C.gio_xdg_surface_listener, unsafe.Pointer(w.surf)) - C.xdg_toplevel_add_listener(w.topLvl, &C.gio_xdg_toplevel_listener, unsafe.Pointer(w.surf)) - - if d.decor != nil { - w.decor = C.zxdg_decoration_manager_v1_get_toplevel_decoration(d.decor, w.topLvl) - C.zxdg_toplevel_decoration_v1_add_listener(w.decor, &C.gio_zxdg_toplevel_decoration_v1_listener, unsafe.Pointer(w.surf)) - } - w.updateOpaqueRegion() - return w, nil -} - -func (w *window) loadCursors() { - w.cursor.cursors.pointer = w.loadCursor(pointer.CursorDefault) - w.cursor.cursors.resizeNorth = w.loadCursor(pointer.CursorNorthResize) - w.cursor.cursors.resizeSouth = w.loadCursor(pointer.CursorSouthResize) - w.cursor.cursors.resizeWest = w.loadCursor(pointer.CursorWestResize) - w.cursor.cursors.resizeEast = w.loadCursor(pointer.CursorEastResize) - w.cursor.cursors.resizeSouthWest = w.loadCursor(pointer.CursorSouthWestResize) - w.cursor.cursors.resizeSouthEast = w.loadCursor(pointer.CursorSouthEastResize) - w.cursor.cursors.resizeNorthWest = w.loadCursor(pointer.CursorNorthWestResize) - w.cursor.cursors.resizeNorthEast = w.loadCursor(pointer.CursorNorthEastResize) -} - -func (w *window) loadCursor(name pointer.Cursor) *C.struct_wl_cursor { - if name == pointer.CursorNone { - return nil - } - xcursor := xCursor[name] - cname := C.CString(xcursor) - defer C.free(unsafe.Pointer(cname)) - c := C.wl_cursor_theme_get_cursor(w.cursor.theme, cname) - if c == nil { - // Fall back to default cursor. - c = w.cursor.cursors.pointer - } - return c -} - -func callbackDelete(k unsafe.Pointer) { - callbackMap.Delete(k) -} - -func callbackStore(k unsafe.Pointer, v interface{}) { - callbackMap.Store(k, v) -} - -func callbackLoad(k unsafe.Pointer) interface{} { - v, exists := callbackMap.Load(k) - if !exists { - panic("missing callback entry") - } - return v -} - -//export gio_onSeatCapabilities -func gio_onSeatCapabilities(data unsafe.Pointer, seat *C.struct_wl_seat, caps C.uint32_t) { - s := callbackLoad(data).(*wlSeat) - s.updateCaps(caps) -} - -// flushOffers remove all wl_data_offers that isn't the clipboard -// content. -func (s *wlSeat) flushOffers() { - for o := range s.offers { - if o == s.clipboard { - continue - } - // We're only interested in clipboard offers. - delete(s.offers, o) - callbackDelete(unsafe.Pointer(o)) - C.wl_data_offer_destroy(o) - } -} - -func (s *wlSeat) destroy() { - if s.source != nil { - C.wl_data_source_destroy(s.source) - s.source = nil - } - if s.im != nil { - C.zwp_text_input_v3_destroy(s.im) - s.im = nil - } - if s.pointer != nil { - C.wl_pointer_release(s.pointer) - } - if s.touch != nil { - C.wl_touch_release(s.touch) - } - if s.keyboard != nil { - C.wl_keyboard_release(s.keyboard) - } - s.clipboard = nil - s.flushOffers() - if s.dataDev != nil { - C.wl_data_device_release(s.dataDev) - } - if s.seat != nil { - callbackDelete(unsafe.Pointer(s.seat)) - C.wl_seat_release(s.seat) - } -} - -func (s *wlSeat) updateCaps(caps C.uint32_t) { - if s.im == nil && s.disp.imm != nil { - s.im = C.zwp_text_input_manager_v3_get_text_input(s.disp.imm, s.seat) - C.zwp_text_input_v3_add_listener(s.im, &C.gio_zwp_text_input_v3_listener, unsafe.Pointer(s.seat)) - } - switch { - case s.pointer == nil && caps&C.WL_SEAT_CAPABILITY_POINTER != 0: - s.pointer = C.wl_seat_get_pointer(s.seat) - C.wl_pointer_add_listener(s.pointer, &C.gio_pointer_listener, unsafe.Pointer(s.seat)) - case s.pointer != nil && caps&C.WL_SEAT_CAPABILITY_POINTER == 0: - C.wl_pointer_release(s.pointer) - s.pointer = nil - } - switch { - case s.touch == nil && caps&C.WL_SEAT_CAPABILITY_TOUCH != 0: - s.touch = C.wl_seat_get_touch(s.seat) - C.wl_touch_add_listener(s.touch, &C.gio_touch_listener, unsafe.Pointer(s.seat)) - case s.touch != nil && caps&C.WL_SEAT_CAPABILITY_TOUCH == 0: - C.wl_touch_release(s.touch) - s.touch = nil - } - switch { - case s.keyboard == nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD != 0: - s.keyboard = C.wl_seat_get_keyboard(s.seat) - C.wl_keyboard_add_listener(s.keyboard, &C.gio_keyboard_listener, unsafe.Pointer(s.seat)) - case s.keyboard != nil && caps&C.WL_SEAT_CAPABILITY_KEYBOARD == 0: - C.wl_keyboard_release(s.keyboard) - s.keyboard = nil - } -} - -//export gio_onSeatName -func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) { -} - -//export gio_onXdgSurfaceConfigure -func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) { - w := callbackLoad(data).(*window) - w.serial = serial - C.xdg_surface_ack_configure(wmSurf, serial) - w.configured = true - w.draw(true) -} - -//export gio_onToplevelClose -func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) { - w := callbackLoad(data).(*window) - w.closing = true -} - -//export gio_onToplevelConfigure -func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, width, height C.int32_t, states *C.struct_wl_array) { - w := callbackLoad(data).(*window) - if width != 0 && height != 0 { - w.size = image.Pt(int(width), int(height)) - w.updateOpaqueRegion() - } -} - -//export gio_onToplevelDecorationConfigure -func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_toplevel_decoration_v1, mode C.uint32_t) { - w := callbackLoad(data).(*window) - decorated := w.config.Decorated - switch mode { - case C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE: - w.config.Decorated = false - case C.ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE: - w.config.Decorated = true - } - if decorated != w.config.Decorated { - w.setWindowConstraints() - if w.config.Decorated { - w.size.Y -= int(w.config.decoHeight) - } else { - w.size.Y += int(w.config.decoHeight) - } - w.ProcessEvent(ConfigEvent{Config: w.config}) - w.draw(true) - } -} - -//export gio_onOutputMode -func gio_onOutputMode(data unsafe.Pointer, output *C.struct_wl_output, flags C.uint32_t, width, height, refresh C.int32_t) { - if flags&C.WL_OUTPUT_MODE_CURRENT == 0 { - return - } - d := callbackLoad(data).(*wlDisplay) - c := d.outputConfig[output] - c.width = int(width) - c.height = int(height) -} - -//export gio_onOutputGeometry -func gio_onOutputGeometry(data unsafe.Pointer, output *C.struct_wl_output, x, y, physWidth, physHeight, subpixel C.int32_t, make, model *C.char, transform C.int32_t) { - d := callbackLoad(data).(*wlDisplay) - c := d.outputConfig[output] - c.transform = transform - c.physWidth = int(physWidth) - c.physHeight = int(physHeight) -} - -//export gio_onOutputScale -func gio_onOutputScale(data unsafe.Pointer, output *C.struct_wl_output, scale C.int32_t) { - d := callbackLoad(data).(*wlDisplay) - c := d.outputConfig[output] - c.scale = int(scale) -} - -//export gio_onOutputDone -func gio_onOutputDone(data unsafe.Pointer, output *C.struct_wl_output) { - d := callbackLoad(data).(*wlDisplay) - conf := d.outputConfig[output] - for _, w := range conf.windows { - w.updateOutputs() - } -} - -//export gio_onSurfaceEnter -func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *C.struct_wl_output) { - w := callbackLoad(data).(*window) - conf := w.disp.outputConfig[output] - var found bool - for _, w2 := range conf.windows { - if w2 == w { - found = true - break - } - } - if !found { - conf.windows = append(conf.windows, w) - } - w.updateOutputs() - if w.config.Mode == Minimized { - // Minimized window got brought back up: it is no longer so. - w.config.Mode = Windowed - w.ProcessEvent(ConfigEvent{Config: w.config}) - } -} - -//export gio_onSurfaceLeave -func gio_onSurfaceLeave(data unsafe.Pointer, surf *C.struct_wl_surface, output *C.struct_wl_output) { - w := callbackLoad(data).(*window) - conf := w.disp.outputConfig[output] - for i, w2 := range conf.windows { - if w2 == w { - conf.windows = append(conf.windows[:i], conf.windows[i+1:]...) - break - } - } - w.updateOutputs() -} - -//export gio_onRegistryGlobal -func gio_onRegistryGlobal(data unsafe.Pointer, reg *C.struct_wl_registry, name C.uint32_t, cintf *C.char, version C.uint32_t) { - d := callbackLoad(data).(*wlDisplay) - switch C.GoString(cintf) { - case "wl_compositor": - d.compositor = (*C.struct_wl_compositor)(C.wl_registry_bind(reg, name, &C.wl_compositor_interface, 3)) - case "wl_output": - output := (*C.struct_wl_output)(C.wl_registry_bind(reg, name, &C.wl_output_interface, 2)) - C.wl_output_add_listener(output, &C.gio_output_listener, unsafe.Pointer(d.disp)) - d.outputMap[name] = output - d.outputConfig[output] = new(wlOutput) - case "wl_seat": - if d.seat != nil { - break - } - s := (*C.struct_wl_seat)(C.wl_registry_bind(reg, name, &C.wl_seat_interface, 5)) - if s == nil { - // No support for v5 protocol. - break - } - d.seat = &wlSeat{ - disp: d, - name: name, - seat: s, - offers: make(map[*C.struct_wl_data_offer][]string), - touchFoci: make(map[C.int32_t]*window), - } - callbackStore(unsafe.Pointer(s), d.seat) - C.wl_seat_add_listener(s, &C.gio_seat_listener, unsafe.Pointer(s)) - d.bindDataDevice() - case "wl_shm": - d.shm = (*C.struct_wl_shm)(C.wl_registry_bind(reg, name, &C.wl_shm_interface, 1)) - case "xdg_wm_base": - d.wm = (*C.struct_xdg_wm_base)(C.wl_registry_bind(reg, name, &C.xdg_wm_base_interface, 1)) - case "zxdg_decoration_manager_v1": - d.decor = (*C.struct_zxdg_decoration_manager_v1)(C.wl_registry_bind(reg, name, &C.zxdg_decoration_manager_v1_interface, 1)) - // TODO: Implement and test text-input support. - /*case "zwp_text_input_manager_v3": - d.imm = (*C.struct_zwp_text_input_manager_v3)(C.wl_registry_bind(reg, name, &C.zwp_text_input_manager_v3_interface, 1))*/ - case "wl_data_device_manager": - d.dataDeviceManager = (*C.struct_wl_data_device_manager)(C.wl_registry_bind(reg, name, &C.wl_data_device_manager_interface, 3)) - d.bindDataDevice() - } -} - -//export gio_onDataOfferOffer -func gio_onDataOfferOffer(data unsafe.Pointer, offer *C.struct_wl_data_offer, mime *C.char) { - s := callbackLoad(data).(*wlSeat) - s.offers[offer] = append(s.offers[offer], C.GoString(mime)) -} - -//export gio_onDataOfferSourceActions -func gio_onDataOfferSourceActions(data unsafe.Pointer, offer *C.struct_wl_data_offer, acts C.uint32_t) { -} - -//export gio_onDataOfferAction -func gio_onDataOfferAction(data unsafe.Pointer, offer *C.struct_wl_data_offer, act C.uint32_t) { -} - -//export gio_onDataDeviceOffer -func gio_onDataDeviceOffer(data unsafe.Pointer, dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) { - s := callbackLoad(data).(*wlSeat) - callbackStore(unsafe.Pointer(id), s) - C.wl_data_offer_add_listener(id, &C.gio_data_offer_listener, unsafe.Pointer(id)) - s.offers[id] = nil -} - -//export gio_onDataDeviceEnter -func gio_onDataDeviceEnter(data unsafe.Pointer, dataDev *C.struct_wl_data_device, serial C.uint32_t, surf *C.struct_wl_surface, x, y C.wl_fixed_t, id *C.struct_wl_data_offer) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial - s.flushOffers() -} - -//export gio_onDataDeviceLeave -func gio_onDataDeviceLeave(data unsafe.Pointer, dataDev *C.struct_wl_data_device) { -} - -//export gio_onDataDeviceMotion -func gio_onDataDeviceMotion(data unsafe.Pointer, dataDev *C.struct_wl_data_device, t C.uint32_t, x, y C.wl_fixed_t) { -} - -//export gio_onDataDeviceDrop -func gio_onDataDeviceDrop(data unsafe.Pointer, dataDev *C.struct_wl_data_device) { -} - -//export gio_onDataDeviceSelection -func gio_onDataDeviceSelection(data unsafe.Pointer, dataDev *C.struct_wl_data_device, id *C.struct_wl_data_offer) { - s := callbackLoad(data).(*wlSeat) - defer s.flushOffers() - s.clipboard = nil -loop: - for _, want := range clipboardMimeTypes { - for _, got := range s.offers[id] { - if want != got { - continue - } - s.clipboard = id - s.mimeType = got - break loop - } - } -} - -//export gio_onRegistryGlobalRemove -func gio_onRegistryGlobalRemove(data unsafe.Pointer, reg *C.struct_wl_registry, name C.uint32_t) { - d := callbackLoad(data).(*wlDisplay) - if s := d.seat; s != nil && name == s.name { - s.destroy() - d.seat = nil - } - if output, exists := d.outputMap[name]; exists { - C.wl_output_destroy(output) - delete(d.outputMap, name) - delete(d.outputConfig, output) - } -} - -//export gio_onTouchDown -func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.uint32_t, surf *C.struct_wl_surface, id C.int32_t, x, y C.wl_fixed_t) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial - w := callbackLoad(unsafe.Pointer(surf)).(*window) - s.touchFoci[id] = w - w.lastTouch = f32.Point{ - X: fromFixed(x) * float32(w.scale), - Y: fromFixed(y) * float32(w.scale), - } - w.ProcessEvent(pointer.Event{ - Kind: pointer.Press, - Source: pointer.Touch, - Position: w.lastTouch, - PointerID: pointer.ID(id), - Time: time.Duration(t) * time.Millisecond, - Modifiers: w.disp.xkb.Modifiers(), - }) -} - -//export gio_onTouchUp -func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.uint32_t, id C.int32_t) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial - w := s.touchFoci[id] - delete(s.touchFoci, id) - w.ProcessEvent(pointer.Event{ - Kind: pointer.Release, - Source: pointer.Touch, - Position: w.lastTouch, - PointerID: pointer.ID(id), - Time: time.Duration(t) * time.Millisecond, - Modifiers: w.disp.xkb.Modifiers(), - }) -} - -//export gio_onTouchMotion -func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32_t, id C.int32_t, x, y C.wl_fixed_t) { - s := callbackLoad(data).(*wlSeat) - w := s.touchFoci[id] - w.lastTouch = f32.Point{ - X: fromFixed(x) * float32(w.scale), - Y: fromFixed(y) * float32(w.scale), - } - w.ProcessEvent(pointer.Event{ - Kind: pointer.Move, - Position: w.lastTouch, - Source: pointer.Touch, - PointerID: pointer.ID(id), - Time: time.Duration(t) * time.Millisecond, - Modifiers: w.disp.xkb.Modifiers(), - }) -} - -//export gio_onTouchFrame -func gio_onTouchFrame(data unsafe.Pointer, touch *C.struct_wl_touch) { -} - -//export gio_onTouchCancel -func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) { - s := callbackLoad(data).(*wlSeat) - for id, w := range s.touchFoci { - delete(s.touchFoci, id) - w.ProcessEvent(pointer.Event{ - Kind: pointer.Cancel, - Source: pointer.Touch, - }) - } -} - -//export gio_onPointerEnter -func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer, serial C.uint32_t, surf *C.struct_wl_surface, x, y C.wl_fixed_t) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial - s.pointerSerial = serial - w := callbackLoad(unsafe.Pointer(surf)).(*window) - s.pointerFocus = w - w.setCursor(pointer, serial) - w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)} -} - -//export gio_onPointerLeave -func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.uint32_t, surf *C.struct_wl_surface) { - w := callbackLoad(unsafe.Pointer(surf)).(*window) - s := callbackLoad(data).(*wlSeat) - s.serial = serial - s.pointerFocus = nil - if w.inCompositor { - w.inCompositor = false - w.ProcessEvent(pointer.Event{Kind: pointer.Cancel}) - } -} - -//export gio_onPointerMotion -func gio_onPointerMotion(data unsafe.Pointer, p *C.struct_wl_pointer, t C.uint32_t, x, y C.wl_fixed_t) { - s := callbackLoad(data).(*wlSeat) - w := s.pointerFocus - w.resetFling() - w.onPointerMotion(x, y, t) -} - -//export gio_onPointerButton -func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t, wbtn, state C.uint32_t) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial - w := s.pointerFocus - // From Linux: include/uapi/linux/input-event-codes.h - const ( - BTN_LEFT = 0x110 - BTN_RIGHT = 0x111 - BTN_MIDDLE = 0x112 - BTN_SIDE = 0x113 - BTN_EXTRA = 0x114 - ) - var btn pointer.Buttons - switch wbtn { - case BTN_LEFT: - btn = pointer.ButtonPrimary - case BTN_RIGHT: - btn = pointer.ButtonSecondary - case BTN_MIDDLE: - btn = pointer.ButtonTertiary - case BTN_SIDE: - btn = pointer.ButtonQuaternary - case BTN_EXTRA: - btn = pointer.ButtonQuinary - default: - return - } - if state == 1 && btn == pointer.ButtonPrimary { - if _, edge := w.systemGesture(); edge != 0 { - w.resize(serial, edge) - return - } - act, ok := w.w.ActionAt(w.lastPos) - if ok && w.config.Mode == Windowed { - switch act { - case system.ActionMove: - w.move(serial) - return - } - } - } - var kind pointer.Kind - switch state { - case 0: - w.pointerBtns &^= btn - kind = pointer.Release - // Move or resize gestures no longer applies. - w.inCompositor = false - case 1: - w.pointerBtns |= btn - kind = pointer.Press - } - w.flushScroll() - w.resetFling() - w.ProcessEvent(pointer.Event{ - Kind: kind, - Source: pointer.Mouse, - Buttons: w.pointerBtns, - Position: w.lastPos, - Time: time.Duration(t) * time.Millisecond, - Modifiers: w.disp.xkb.Modifiers(), - }) -} - -//export gio_onPointerAxis -func gio_onPointerAxis(data unsafe.Pointer, p *C.struct_wl_pointer, t, axis C.uint32_t, value C.wl_fixed_t) { - s := callbackLoad(data).(*wlSeat) - w := s.pointerFocus - v := fromFixed(value) - w.resetFling() - if w.scroll.dist == (f32.Point{}) { - w.scroll.time = time.Duration(t) * time.Millisecond - } - switch axis { - case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL: - w.scroll.dist.X += v - case C.WL_POINTER_AXIS_VERTICAL_SCROLL: - // horizontal scroll if shift + mousewheel(up/down) pressed. - if w.disp.xkb.Modifiers() == key.ModShift { - w.scroll.dist.X += v - } else { - w.scroll.dist.Y += v - } - } -} - -//export gio_onPointerFrame -func gio_onPointerFrame(data unsafe.Pointer, p *C.struct_wl_pointer) { - s := callbackLoad(data).(*wlSeat) - w := s.pointerFocus - if w == nil { - return - } - w.flushScroll() - w.flushFling() -} - -func (w *window) flushFling() { - if !w.fling.start { - return - } - w.fling.start = false - estx, esty := w.fling.xExtrapolation.Estimate(), w.fling.yExtrapolation.Estimate() - w.fling.xExtrapolation = fling.Extrapolation{} - w.fling.yExtrapolation = fling.Extrapolation{} - vel := float32(math.Sqrt(float64(estx.Velocity*estx.Velocity + esty.Velocity*esty.Velocity))) - _, c := w.getConfig() - if !w.fling.anim.Start(c, time.Now(), vel) { - return - } - invDist := 1 / vel - w.fling.dir.X = estx.Velocity * invDist - w.fling.dir.Y = esty.Velocity * invDist -} - -//export gio_onPointerAxisSource -func gio_onPointerAxisSource(data unsafe.Pointer, pointer *C.struct_wl_pointer, source C.uint32_t) { -} - -//export gio_onPointerAxisStop -func gio_onPointerAxisStop(data unsafe.Pointer, p *C.struct_wl_pointer, t, axis C.uint32_t) { - s := callbackLoad(data).(*wlSeat) - w := s.pointerFocus - w.fling.start = true -} - -//export gio_onPointerAxisDiscrete -func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis C.uint32_t, discrete C.int32_t) { - s := callbackLoad(data).(*wlSeat) - w := s.pointerFocus - w.resetFling() - switch axis { - case C.WL_POINTER_AXIS_HORIZONTAL_SCROLL: - w.scroll.steps.X += int(discrete) - case C.WL_POINTER_AXIS_VERTICAL_SCROLL: - // horizontal scroll if shift + mousewheel(up/down) pressed. - if w.disp.xkb.Modifiers() == key.ModShift { - w.scroll.steps.X += int(discrete) - } else { - w.scroll.steps.Y += int(discrete) - } - } -} - -func (w *window) ReadClipboard() { - if w.disp.readClipClose != nil { - return - } - w.disp.readClipClose = make(chan struct{}) - r, err := w.disp.readClipboard() - if r == nil || err != nil { - return - } - // Don't let slow clipboard transfers block event loop. - go func() { - defer r.Close() - data, _ := io.ReadAll(r) - e := transfer.DataEvent{ - Type: "application/text", - Open: func() io.ReadCloser { - return io.NopCloser(bytes.NewReader(data)) - }, - } - select { - case w.clipReads <- e: - w.disp.wakeup() - case <-w.disp.readClipClose: - } - }() -} - -func (w *window) WriteClipboard(mime string, s []byte) { - w.disp.writeClipboard(s) -} - -func (w *window) Configure(options []Option) { - _, cfg := w.getConfig() - prev := w.config - cnf := w.config - cnf.apply(cfg, options) - w.config.decoHeight = cnf.decoHeight - - switch cnf.Mode { - case Fullscreen: - switch prev.Mode { - case Minimized, Fullscreen: - default: - w.config.Mode = Fullscreen - w.wsize = w.config.Size - C.xdg_toplevel_set_fullscreen(w.topLvl, nil) - } - case Minimized: - switch prev.Mode { - case Minimized, Fullscreen: - default: - w.config.Mode = Minimized - C.xdg_toplevel_set_minimized(w.topLvl) - } - case Maximized: - switch prev.Mode { - case Minimized, Fullscreen: - default: - w.config.Mode = Maximized - w.wsize = w.config.Size - C.xdg_toplevel_set_maximized(w.topLvl) - w.setTitle(prev, cnf) - } - case Windowed: - switch prev.Mode { - case Fullscreen: - w.config.Mode = Windowed - w.size = w.wsize.Div(w.scale) - C.xdg_toplevel_unset_fullscreen(w.topLvl) - case Minimized: - w.config.Mode = Windowed - case Maximized: - w.config.Mode = Windowed - w.size = w.wsize.Div(w.scale) - C.xdg_toplevel_unset_maximized(w.topLvl) - } - w.setTitle(prev, cnf) - if prev.Size != cnf.Size { - w.config.Size = cnf.Size - w.config.Size.Y += int(w.decoHeight()) * w.scale - w.size = w.config.Size.Div(w.scale) - } - w.config.MinSize = cnf.MinSize - w.config.MaxSize = cnf.MaxSize - w.setWindowConstraints() - } - w.ProcessEvent(ConfigEvent{Config: w.config}) -} - -func (w *window) setWindowConstraints() { - decoHeight := w.decoHeight() - if scaled := w.config.MinSize.Div(w.scale); scaled != (image.Point{}) { - C.xdg_toplevel_set_min_size(w.topLvl, C.int32_t(scaled.X), C.int32_t(scaled.Y+decoHeight)) - } - if scaled := w.config.MaxSize.Div(w.scale); scaled != (image.Point{}) { - C.xdg_toplevel_set_max_size(w.topLvl, C.int32_t(scaled.X), C.int32_t(scaled.Y+decoHeight)) - } -} - -// decoHeight returns the adjustment for client-side decorations, if applicable. -// The unit is in surface-local coordinates. -func (w *window) decoHeight() int { - if !w.config.Decorated { - return int(w.config.decoHeight) - } - return 0 -} - -func (w *window) setTitle(prev, cnf Config) { - if prev.Title != cnf.Title { - w.config.Title = cnf.Title - title := C.CString(cnf.Title) - C.xdg_toplevel_set_title(w.topLvl, title) - C.free(unsafe.Pointer(title)) - } -} - -func (w *window) Perform(actions system.Action) { - // NB. there is no way for a minimized window to be unminimized. - // https://wayland.app/protocols/xdg-shell#xdg_toplevel:request:set_minimized - walkActions(actions, func(action system.Action) { - switch action { - case system.ActionClose: - w.closing = true - } - }) -} - -func (w *window) move(serial C.uint32_t) { - s := w.disp.seat - if w.inCompositor || s.pointerFocus != w { - return - } - w.inCompositor = true - C.xdg_toplevel_move(w.topLvl, s.seat, serial) -} - -func (w *window) resize(serial, edge C.uint32_t) { - s := w.disp.seat - if w.inCompositor || s.pointerFocus != w { - return - } - w.inCompositor = true - C.xdg_toplevel_resize(w.topLvl, s.seat, serial, edge) -} - -func (w *window) SetCursor(cursor pointer.Cursor) { - w.cursor.cursor = w.loadCursor(cursor) - w.updateCursor() -} - -func (w *window) updateCursor() { - s := w.disp.seat - ptr := s.pointer - if ptr == nil || s.pointerFocus != w { - return - } - w.setCursor(ptr, s.pointerSerial) -} - -func (w *window) setCursor(pointer *C.struct_wl_pointer, serial C.uint32_t) { - c := w.cursor.system - if c == nil { - c = w.cursor.cursor - } - if c == nil { - C.wl_pointer_set_cursor(pointer, serial, nil, 0, 0) - return - } - // Get images[0]. - img := *c.images - buf := C.wl_cursor_image_get_buffer(img) - if buf == nil { - return - } - C.wl_pointer_set_cursor(pointer, serial, w.cursor.surf, C.int32_t(img.hotspot_x/C.uint(w.scale)), C.int32_t(img.hotspot_y/C.uint(w.scale))) - C.wl_surface_attach(w.cursor.surf, buf, 0, 0) - C.wl_surface_damage(w.cursor.surf, 0, 0, C.int32_t(img.width), C.int32_t(img.height)) - C.wl_surface_commit(w.cursor.surf) -} - -func (w *window) resetFling() { - w.fling.start = false - w.fling.anim = fling.Animation{} -} - -//export gio_onKeyboardKeymap -func gio_onKeyboardKeymap(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, format C.uint32_t, fd C.int32_t, size C.uint32_t) { - defer syscall.Close(int(fd)) - s := callbackLoad(data).(*wlSeat) - s.disp.repeat.Stop(0) - s.disp.xkb.DestroyKeymapState() - if format != C.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 { - return - } - if err := s.disp.xkb.LoadKeymap(int(format), int(fd), int(size)); err != nil { - // TODO: Do better. - panic(err) - } -} - -//export gio_onKeyboardEnter -func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial C.uint32_t, surf *C.struct_wl_surface, keys *C.struct_wl_array) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial - w := callbackLoad(unsafe.Pointer(surf)).(*window) - s.keyboardFocus = w - s.disp.repeat.Stop(0) - w.config.Focused = true - w.ProcessEvent(ConfigEvent{Config: w.config}) -} - -//export gio_onKeyboardLeave -func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial C.uint32_t, surf *C.struct_wl_surface) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial - s.disp.repeat.Stop(0) - w := s.keyboardFocus - w.config.Focused = false - w.ProcessEvent(ConfigEvent{Config: w.config}) -} - -//export gio_onKeyboardKey -func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial, timestamp, keyCode, state C.uint32_t) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial - w := s.keyboardFocus - t := time.Duration(timestamp) * time.Millisecond - s.disp.repeat.Stop(t) - w.resetFling() - kc := mapXKBKeycode(uint32(keyCode)) - ks := mapXKBKeyState(uint32(state)) - for _, e := range w.disp.xkb.DispatchKey(kc, ks) { - if ee, ok := e.(key.EditEvent); ok { - // There's no support for IME yet. - w.w.EditorInsert(ee.Text) - } else { - w.ProcessEvent(e) - } - } - if state != C.WL_KEYBOARD_KEY_STATE_PRESSED { - return - } - if w.disp.xkb.IsRepeatKey(kc) { - w.disp.repeat.Start(w, kc, t) - } -} - -func mapXKBKeycode(keyCode uint32) uint32 { - // According to the xkb_v1 spec: "to determine the xkb keycode, clients must add 8 to the key event keycode." - return keyCode + 8 -} - -func mapXKBKeyState(state uint32) key.State { - switch state { - case C.WL_KEYBOARD_KEY_STATE_RELEASED: - return key.Release - default: - return key.Press - } -} - -func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) { - if r.rate <= 0 { - return - } - stopC := make(chan struct{}) - r.start = t - r.last = 0 - r.now = 0 - r.stopC = stopC - r.key = keyCode - r.win = w - rate, delay := r.rate, r.delay - go func() { - timer := time.NewTimer(delay) - for { - select { - case <-timer.C: - case <-stopC: - close(stopC) - return - } - r.Advance(delay) - w.disp.wakeup() - delay = time.Second / time.Duration(rate) - timer.Reset(delay) - } - }() -} - -func (r *repeatState) Stop(t time.Duration) { - if r.stopC == nil { - return - } - r.stopC <- struct{}{} - <-r.stopC - r.stopC = nil - t -= r.start - if r.now > t { - r.now = t - } -} - -func (r *repeatState) Advance(dt time.Duration) { - r.mu.Lock() - defer r.mu.Unlock() - r.now += dt -} - -func (r *repeatState) Repeat(d *wlDisplay) { - if r.rate <= 0 { - return - } - r.mu.Lock() - now := r.now - r.mu.Unlock() - for { - var delay time.Duration - if r.last < r.delay { - delay = r.delay - } else { - delay = time.Second / time.Duration(r.rate) - } - if r.last+delay > now { - break - } - for _, e := range d.xkb.DispatchKey(r.key, key.Press) { - if ee, ok := e.(key.EditEvent); ok { - // There's no support for IME yet. - r.win.w.EditorInsert(ee.Text) - } else { - r.win.ProcessEvent(e) - } - } - r.last += delay - } -} - -//export gio_onFrameDone -func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.uint32_t) { - C.wl_callback_destroy(callback) - w := callbackLoad(data).(*window) - if w.lastFrameCallback == callback { - w.lastFrameCallback = nil - w.draw(false) - } -} - -func (w *window) close(err error) { - w.ProcessEvent(WaylandViewEvent{}) - w.ProcessEvent(DestroyEvent{Err: err}) - w.destroy() - w.disp.destroy() - w.disp = nil -} - -func (w *window) dispatch() { - if w.disp == nil { - <-w.wakeups - w.w.Invalidate() - return - } - if err := w.disp.dispatch(); err != nil || w.closing { - w.close(err) - return - } - select { - case e := <-w.clipReads: - w.disp.readClipClose = nil - w.ProcessEvent(e) - case <-w.wakeups: - w.w.Invalidate() - default: - } -} - -func (w *window) ProcessEvent(e event.Event) { - w.w.ProcessEvent(e) -} - -func (w *window) Event() event.Event { - for { - evt, ok := w.w.nextEvent() - if !ok { - w.dispatch() - continue - } - return evt - } -} - -func (w *window) Invalidate() { - select { - case w.wakeups <- struct{}{}: - default: - return - } - w.disp.wakeup() -} - -func (w *window) Run(f func()) { - f() -} - -func (w *window) Frame(frame *op.Ops) { - w.w.ProcessFrame(frame, nil) -} - -// bindDataDevice initializes the dataDev field if and only if both -// the seat and dataDeviceManager fields are initialized. -func (d *wlDisplay) bindDataDevice() { - if d.seat != nil && d.dataDeviceManager != nil { - d.seat.dataDev = C.wl_data_device_manager_get_data_device(d.dataDeviceManager, d.seat.seat) - if d.seat.dataDev == nil { - return - } - callbackStore(unsafe.Pointer(d.seat.dataDev), d.seat) - C.wl_data_device_add_listener(d.seat.dataDev, &C.gio_data_device_listener, unsafe.Pointer(d.seat.dataDev)) - } -} - -func (d *wlDisplay) dispatch() error { - // wl_display_prepare_read records the current thread for - // use in wl_display_read_events or wl_display_cancel_events. - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - dispfd := C.wl_display_get_fd(d.disp) - // Poll for events and notifications. - pollfds := append(d.poller.pollfds[:0], - syscall.PollFd{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR}, - syscall.PollFd{Fd: int32(d.notify.read), Events: syscall.POLLIN | syscall.POLLERR}, - ) - for C.wl_display_prepare_read(d.disp) != 0 { - C.wl_display_dispatch_pending(d.disp) - } - dispFd := &pollfds[0] - if ret, err := C.wl_display_flush(d.disp); ret < 0 { - if err != syscall.EAGAIN { - return fmt.Errorf("wayland: wl_display_flush failed: %v", err) - } - // EAGAIN means the output buffer was full. Poll for - // POLLOUT to know when we can write again. - dispFd.Events |= syscall.POLLOUT - } - if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { - C.wl_display_cancel_read(d.disp) - return fmt.Errorf("wayland: poll failed: %v", err) - } - if dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0 { - C.wl_display_cancel_read(d.disp) - return errors.New("wayland: display file descriptor gone") - } - // Handle events. - if dispFd.Revents&syscall.POLLIN != 0 { - if ret, err := C.wl_display_read_events(d.disp); ret < 0 { - return fmt.Errorf("wayland: wl_display_read_events failed: %v", err) - } - C.wl_display_dispatch_pending(d.disp) - } else { - C.wl_display_cancel_read(d.disp) - } - // Clear notifications. - for { - _, err := syscall.Read(d.notify.read, d.poller.buf[:]) - if err == syscall.EAGAIN { - break - } - if err != nil { - return fmt.Errorf("wayland: read from notify pipe failed: %v", err) - } - } - d.repeat.Repeat(d) - return nil -} - -func (w *window) SetAnimating(anim bool) { - w.animating = anim - if anim { - w.draw(false) - } -} - -// Wakeup wakes up the event loop through the notification pipe. -func (d *wlDisplay) wakeup() { - oneByte := make([]byte, 1) - if _, err := syscall.Write(d.notify.write, oneByte); err != nil && err != syscall.EAGAIN { - panic(fmt.Errorf("failed to write to pipe: %v", err)) - } -} - -func (w *window) destroy() { - if w.lastFrameCallback != nil { - C.wl_callback_destroy(w.lastFrameCallback) - w.lastFrameCallback = nil - } - if w.cursor.surf != nil { - C.wl_surface_destroy(w.cursor.surf) - } - if w.cursor.theme != nil { - C.wl_cursor_theme_destroy(w.cursor.theme) - } - if w.topLvl != nil { - C.xdg_toplevel_destroy(w.topLvl) - } - if w.surf != nil { - C.wl_surface_destroy(w.surf) - } - if w.wmSurf != nil { - C.xdg_surface_destroy(w.wmSurf) - } - if w.decor != nil { - C.zxdg_toplevel_decoration_v1_destroy(w.decor) - } - callbackDelete(unsafe.Pointer(w.surf)) -} - -//export gio_onKeyboardModifiers -func gio_onKeyboardModifiers(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, serial, depressed, latched, locked, group C.uint32_t) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial - d := s.disp - d.repeat.Stop(0) - if d.xkb == nil { - return - } - d.xkb.UpdateMask(uint32(depressed), uint32(latched), uint32(locked), uint32(group), uint32(group), uint32(group)) -} - -//export gio_onKeyboardRepeatInfo -func gio_onKeyboardRepeatInfo(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, rate, delay C.int32_t) { - s := callbackLoad(data).(*wlSeat) - d := s.disp - d.repeat.Stop(0) - d.repeat.rate = int(rate) - d.repeat.delay = time.Duration(delay) * time.Millisecond -} - -//export gio_onTextInputEnter -func gio_onTextInputEnter(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, surf *C.struct_wl_surface) { -} - -//export gio_onTextInputLeave -func gio_onTextInputLeave(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, surf *C.struct_wl_surface) { -} - -//export gio_onTextInputPreeditString -func gio_onTextInputPreeditString(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, ctxt *C.char, begin, end C.int32_t) { -} - -//export gio_onTextInputCommitString -func gio_onTextInputCommitString(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, ctxt *C.char) { -} - -//export gio_onTextInputDeleteSurroundingText -func gio_onTextInputDeleteSurroundingText(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, before, after C.uint32_t) { -} - -//export gio_onTextInputDone -func gio_onTextInputDone(data unsafe.Pointer, im *C.struct_zwp_text_input_v3, serial C.uint32_t) { - s := callbackLoad(data).(*wlSeat) - s.serial = serial -} - -//export gio_onDataSourceTarget -func gio_onDataSourceTarget(data unsafe.Pointer, source *C.struct_wl_data_source, mime *C.char) { -} - -//export gio_onDataSourceSend -func gio_onDataSourceSend(data unsafe.Pointer, source *C.struct_wl_data_source, mime *C.char, fd C.int32_t) { - s := callbackLoad(data).(*wlSeat) - content := s.content - go func() { - defer syscall.Close(int(fd)) - syscall.Write(int(fd), content) - }() -} - -//export gio_onDataSourceCancelled -func gio_onDataSourceCancelled(data unsafe.Pointer, source *C.struct_wl_data_source) { - s := callbackLoad(data).(*wlSeat) - if s.source == source { - s.content = nil - s.source = nil - } - C.wl_data_source_destroy(source) -} - -//export gio_onDataSourceDNDDropPerformed -func gio_onDataSourceDNDDropPerformed(data unsafe.Pointer, source *C.struct_wl_data_source) { -} - -//export gio_onDataSourceDNDFinished -func gio_onDataSourceDNDFinished(data unsafe.Pointer, source *C.struct_wl_data_source) { -} - -//export gio_onDataSourceAction -func gio_onDataSourceAction(data unsafe.Pointer, source *C.struct_wl_data_source, act C.uint32_t) { -} - -func (w *window) flushScroll() { - var fling f32.Point - if w.fling.anim.Active() { - dist := float32(w.fling.anim.Tick(time.Now())) - fling = w.fling.dir.Mul(dist) - } - // The Wayland reported scroll distance for - // discrete scroll axes is only 10 pixels, where - // 100 seems more appropriate. - const discreteScale = 10 - if w.scroll.steps.X != 0 { - w.scroll.dist.X *= discreteScale - } - if w.scroll.steps.Y != 0 { - w.scroll.dist.Y *= discreteScale - } - total := w.scroll.dist.Add(fling) - if total == (f32.Point{}) { - return - } - if w.scroll.steps == (image.Point{}) { - w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X) - w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y) - } - // Zero scroll distance prior to calling ProcessEvent, otherwise we may recursively - // re-process the scroll distance. - w.scroll.dist = f32.Point{} - w.scroll.steps = image.Point{} - w.ProcessEvent(pointer.Event{ - Kind: pointer.Scroll, - Source: pointer.Mouse, - Buttons: w.pointerBtns, - Position: w.lastPos, - Scroll: total, - Time: w.scroll.time, - Modifiers: w.disp.xkb.Modifiers(), - }) -} - -func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) { - w.flushScroll() - w.lastPos = f32.Point{ - X: fromFixed(x) * float32(w.scale), - Y: fromFixed(y) * float32(w.scale), - } - w.ProcessEvent(pointer.Event{ - Kind: pointer.Move, - Position: w.lastPos, - Buttons: w.pointerBtns, - Source: pointer.Mouse, - Time: time.Duration(t) * time.Millisecond, - Modifiers: w.disp.xkb.Modifiers(), - }) - c, _ := w.systemGesture() - if c != w.cursor.system { - w.cursor.system = c - w.updateCursor() - } -} - -// updateCursor updates the system gesture cursor according to the pointer -// position. -func (w *window) systemGesture() (*C.struct_wl_cursor, C.uint32_t) { - if w.config.Mode != Windowed || w.config.Decorated { - return nil, 0 - } - _, cfg := w.getConfig() - border := cfg.Dp(3) - x, y, size := int(w.lastPos.X), int(w.lastPos.Y), w.config.Size - north := y <= border - south := y >= size.Y-border - west := x <= border - east := x >= size.X-border - - switch { - default: - fallthrough - case !north && !south && !west && !east: - return nil, 0 - case north && west: - return w.cursor.cursors.resizeNorthWest, C.XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT - case north && east: - return w.cursor.cursors.resizeNorthEast, C.XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT - case south && west: - return w.cursor.cursors.resizeSouthWest, C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT - case south && east: - return w.cursor.cursors.resizeSouthEast, C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT - case north: - return w.cursor.cursors.resizeNorth, C.XDG_TOPLEVEL_RESIZE_EDGE_TOP - case south: - return w.cursor.cursors.resizeSouth, C.XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM - case west: - return w.cursor.cursors.resizeWest, C.XDG_TOPLEVEL_RESIZE_EDGE_LEFT - case east: - return w.cursor.cursors.resizeEast, C.XDG_TOPLEVEL_RESIZE_EDGE_RIGHT - } -} - -func (w *window) updateOpaqueRegion() { - reg := C.wl_compositor_create_region(w.disp.compositor) - C.wl_region_add(reg, 0, 0, C.int32_t(w.size.X), C.int32_t(w.size.Y)) - C.wl_surface_set_opaque_region(w.surf, reg) - C.wl_region_destroy(reg) -} - -func (w *window) updateOutputs() { - scale := 1 - var found bool - for _, conf := range w.disp.outputConfig { - for _, w2 := range conf.windows { - if w2 == w { - found = true - if conf.scale > scale { - scale = conf.scale - } - } - } - } - if found && scale != w.scale { - w.scale = scale - C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale)) - w.draw(true) - } - if found { - w.draw(true) - } -} - -func (w *window) getConfig() (image.Point, unit.Metric) { - size := w.size.Mul(w.scale) - return size, unit.Metric{ - PxPerDp: w.ppdp * float32(w.scale), - PxPerSp: w.ppsp * float32(w.scale), - } -} - -func (w *window) draw(sync bool) { - if !w.configured { - return - } - w.flushScroll() - size, cfg := w.getConfig() - if cfg == (unit.Metric{}) { - return - } - if size != w.config.Size { - w.config.Size = size - w.ProcessEvent(ConfigEvent{Config: w.config}) - } - anim := w.animating || w.fling.anim.Active() - // Draw animation only when not waiting for frame callback. - redrawAnim := anim && w.lastFrameCallback == nil - if !redrawAnim && !sync { - return - } - if anim { - w.lastFrameCallback = C.wl_surface_frame(w.surf) - // Use the surface as listener data for gio_onFrameDone. - C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf)) - } - w.ProcessEvent(frameEvent{ - FrameEvent: FrameEvent{ - Now: time.Now(), - Size: w.config.Size, - Metric: cfg, - }, - Sync: sync, - }) -} - -func (w *window) display() *C.struct_wl_display { - return w.disp.disp -} - -func (w *window) surface() (*C.struct_wl_surface, int, int) { - sz, _ := w.getConfig() - return w.surf, sz.X, sz.Y -} - -func (w *window) ShowTextInput(show bool) {} - -func (w *window) SetInputHint(_ key.InputHint) {} - -func (w *window) EditorStateChanged(old, new editorState) {} - -func (w *window) NewContext() (context, error) { - var firstErr error - if f := newWaylandEGLContext; f != nil { - c, err := f(w) - if err == nil { - return c, nil - } - firstErr = err - } - if f := newWaylandVulkanContext; f != nil { - c, err := f(w) - if err == nil { - return c, nil - } - firstErr = err - } - if firstErr != nil { - return nil, firstErr - } - return nil, errors.New("wayland: no available GPU backends") -} - -// detectUIScale reports the system UI scale, or 1.0 if it fails. -func detectUIScale() float32 { - // TODO: What about other window environments? - out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "text-scaling-factor").Output() - if err != nil { - return 1.0 - } - scale, err := strconv.ParseFloat(string(bytes.TrimSpace(out)), 32) - if err != nil { - return 1.0 - } - return float32(scale) -} - -func newWLDisplay() (*wlDisplay, error) { - d := &wlDisplay{ - outputMap: make(map[C.uint32_t]*C.struct_wl_output), - outputConfig: make(map[*C.struct_wl_output]*wlOutput), - } - pipe := make([]int, 2) - if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil { - return nil, fmt.Errorf("wayland: failed to create pipe: %v", err) - } - d.notify.read = pipe[0] - d.notify.write = pipe[1] - xkb, err := xkb.New() - if err != nil { - d.destroy() - return nil, fmt.Errorf("wayland: %v", err) - } - d.xkb = xkb - d.disp, err = C.wl_display_connect(nil) - if d.disp == nil { - d.destroy() - return nil, fmt.Errorf("wayland: wl_display_connect failed: %v", err) - } - callbackMap.Store(unsafe.Pointer(d.disp), d) - d.reg = C.wl_display_get_registry(d.disp) - if d.reg == nil { - d.destroy() - return nil, errors.New("wayland: wl_display_get_registry failed") - } - C.wl_registry_add_listener(d.reg, &C.gio_registry_listener, unsafe.Pointer(d.disp)) - // Wait for the server to register all its globals to the - // registry listener (gio_onRegistryGlobal). - C.wl_display_roundtrip(d.disp) - // Configuration listeners are added to outputs by gio_onRegistryGlobal. - // We need another roundtrip to get the initial output configurations - // through the gio_onOutput* callbacks. - C.wl_display_roundtrip(d.disp) - return d, nil -} - -func (d *wlDisplay) destroy() { - if d.readClipClose != nil { - close(d.readClipClose) - d.readClipClose = nil - } - if d.notify.write != 0 { - syscall.Close(d.notify.write) - d.notify.write = 0 - } - if d.notify.read != 0 { - syscall.Close(d.notify.read) - d.notify.read = 0 - } - d.repeat.Stop(0) - if d.xkb != nil { - d.xkb.Destroy() - d.xkb = nil - } - if d.seat != nil { - d.seat.destroy() - d.seat = nil - } - if d.imm != nil { - C.zwp_text_input_manager_v3_destroy(d.imm) - } - if d.decor != nil { - C.zxdg_decoration_manager_v1_destroy(d.decor) - } - if d.shm != nil { - C.wl_shm_destroy(d.shm) - } - if d.compositor != nil { - C.wl_compositor_destroy(d.compositor) - } - if d.wm != nil { - C.xdg_wm_base_destroy(d.wm) - } - for _, output := range d.outputMap { - C.wl_output_destroy(output) - } - if d.reg != nil { - C.wl_registry_destroy(d.reg) - } - if d.disp != nil { - C.wl_display_disconnect(d.disp) - callbackDelete(unsafe.Pointer(d.disp)) - d.disp = nil - } -} - -// fromFixed converts a Wayland wl_fixed_t 23.8 number to float32. -func fromFixed(v C.wl_fixed_t) float32 { - // Convert to float64 to avoid overflow. - // From wayland-util.h. - b := ((1023 + 44) << 52) + (1 << 51) + uint64(v) - f := math.Float64frombits(b) - (3 << 43) - return float32(f) -} diff --git a/gio/app/os_windows.go b/gio/app/os_windows.go deleted file mode 100644 index b6a8123..0000000 --- a/gio/app/os_windows.go +++ /dev/null @@ -1,1054 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "errors" - "fmt" - "image" - "io" - "runtime" - "sort" - "strings" - "sync" - "time" - "unicode" - "unicode/utf8" - "unsafe" - - syscall "golang.org/x/sys/windows" - - "github.com/p9c/p9/pkg/gel/gio/app/internal/windows" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" - gowindows "golang.org/x/sys/windows" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" -) - -type Win32ViewEvent struct { - HWND uintptr -} - -type window struct { - hwnd syscall.Handle - hdc syscall.Handle - w *callbacks - - // cursorIn tracks whether the cursor was inside the window according - // to the most recent WM_SETCURSOR. - cursorIn bool - cursor syscall.Handle - - animating bool - - borderSize image.Point - config Config - // frameDims stores the last seen window frame width and height. - frameDims image.Point - loop *eventLoop -} - -const _WM_WAKEUP = windows.WM_USER + iota - -type gpuAPI struct { - priority int - initializer func(w *window) (context, error) -} - -// drivers is the list of potential Context implementations. -var drivers []gpuAPI - -// winMap maps win32 HWNDs to *windows. -var winMap sync.Map - -// iconID is the ID of the icon in the resource file. -const iconID = 1 - -var resources struct { - once sync.Once - // handle is the module handle from GetModuleHandle. - handle syscall.Handle - // class is the Gio window class from RegisterClassEx. - class uint16 - // cursor is the arrow cursor resource. - cursor syscall.Handle -} - -func osMain() { - select {} -} - -func newWindow(win *callbacks, options []Option) { - done := make(chan struct{}) - go func() { - // GetMessage and PeekMessage can filter on a window HWND, but - // then thread-specific messages such as WM_QUIT are ignored. - // Instead lock the thread so window messages arrive through - // unfiltered GetMessage calls. - runtime.LockOSThread() - - w := &window{ - w: win, - } - w.loop = newEventLoop(w.w, w.wakeup) - w.w.SetDriver(w) - err := w.init() - done <- struct{}{} - if err != nil { - w.ProcessEvent(DestroyEvent{Err: err}) - return - } - winMap.Store(w.hwnd, w) - defer winMap.Delete(w.hwnd) - w.Configure(options) - w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)}) - windows.SetForegroundWindow(w.hwnd) - windows.SetFocus(w.hwnd) - // Since the window class for the cursor is null, - // set it here to show the cursor. - w.SetCursor(pointer.CursorDefault) - w.runLoop() - }() - <-done -} - -// initResources initializes the resources global. -func initResources() error { - windows.SetProcessDPIAware() - hInst, err := windows.GetModuleHandle() - if err != nil { - return err - } - resources.handle = hInst - c, err := windows.LoadCursor(windows.IDC_ARROW) - if err != nil { - return err - } - resources.cursor = c - icon, _ := windows.LoadImage(hInst, iconID, windows.IMAGE_ICON, 0, 0, windows.LR_DEFAULTSIZE|windows.LR_SHARED) - wcls := windows.WndClassEx{ - CbSize: uint32(unsafe.Sizeof(windows.WndClassEx{})), - Style: windows.CS_HREDRAW | windows.CS_VREDRAW | windows.CS_OWNDC, - LpfnWndProc: syscall.NewCallback(windowProc), - HInstance: hInst, - HIcon: icon, - LpszClassName: syscall.StringToUTF16Ptr("GioWindow"), - } - cls, err := windows.RegisterClassEx(&wcls) - if err != nil { - return err - } - resources.class = cls - return nil -} - -const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE - -func (w *window) init() error { - var resErr error - resources.once.Do(func() { - resErr = initResources() - }) - if resErr != nil { - return resErr - } - const dwStyle = windows.WS_OVERLAPPEDWINDOW - - hwnd, err := windows.CreateWindowEx( - dwExStyle, - resources.class, - "", - dwStyle|windows.WS_CLIPSIBLINGS|windows.WS_CLIPCHILDREN, - windows.CW_USEDEFAULT, windows.CW_USEDEFAULT, - windows.CW_USEDEFAULT, windows.CW_USEDEFAULT, - 0, - 0, - resources.handle, - 0) - if err != nil { - return err - } - if err := windows.RegisterTouchWindow(hwnd, 0); err != nil { - return err - } - if err := windows.EnableMouseInPointer(1); err != nil { - return err - } - w.hdc, err = windows.GetDC(hwnd) - if err != nil { - windows.DestroyWindow(hwnd) - return err - } - w.hwnd = hwnd - return nil -} - -// update handles changes done by the user, and updates the configuration. -// It reads the window style and size/position and updates w.config. -// If anything has changed it emits a ConfigEvent to notify the application. -func (w *window) update() { - p := windows.GetWindowPlacement(w.hwnd) - if !p.IsMinimized() { - r := windows.GetWindowRect(w.hwnd) - cr := windows.GetClientRect(w.hwnd) - w.config.Size = image.Point{ - X: int(cr.Right - cr.Left), - Y: int(cr.Bottom - cr.Top), - } - w.frameDims = image.Point{ - X: int(r.Right - r.Left), - Y: int(r.Bottom - r.Top), - }.Sub(w.config.Size) - } - - w.borderSize = image.Pt( - windows.GetSystemMetrics(windows.SM_CXSIZEFRAME), - windows.GetSystemMetrics(windows.SM_CYSIZEFRAME), - ) - style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE) - switch { - case p.IsMaximized() && style&windows.WS_OVERLAPPEDWINDOW != 0: - w.config.Mode = Maximized - case p.IsMaximized(): - w.config.Mode = Fullscreen - default: - w.config.Mode = Windowed - } - w.ProcessEvent(ConfigEvent{Config: w.config}) - w.draw(true) -} - -func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr { - win, exists := winMap.Load(hwnd) - if !exists { - return windows.DefWindowProc(hwnd, msg, wParam, lParam) - } - - w := win.(*window) - - switch msg { - case windows.WM_UNICHAR: - if wParam == windows.UNICODE_NOCHAR { - // Tell the system that we accept WM_UNICHAR messages. - return windows.TRUE - } - fallthrough - case windows.WM_CHAR: - if r := rune(wParam); unicode.IsPrint(r) { - w.w.EditorInsert(string(r)) - } - // The message is processed. - return windows.TRUE - case windows.WM_DPICHANGED: - // Let Windows know we're prepared for runtime DPI changes. - return windows.TRUE - case windows.WM_ERASEBKGND: - // Avoid flickering between GPU content and background color. - return windows.TRUE - case windows.WM_KEYDOWN, windows.WM_KEYUP, windows.WM_SYSKEYDOWN, windows.WM_SYSKEYUP: - if n, ok := convertKeyCode(wParam); ok { - e := key.Event{ - Name: n, - Modifiers: getModifiers(), - State: key.Press, - } - if msg == windows.WM_KEYUP || msg == windows.WM_SYSKEYUP { - e.State = key.Release - } - - w.ProcessEvent(e) - - if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) { - // Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs - // such as cmd.exe and graphical debuggers also reserve F10. - return 0 - } - } - case windows.WM_POINTERDOWN, windows.WM_POINTERUP, windows.WM_POINTERUPDATE, windows.WM_POINTERCAPTURECHANGED: - pid := getPointerIDwParam(wParam) - pi, err := windows.GetPointerInfo(uint32(pid)) - if err != nil { - panic(err) - } - switch msg { - case windows.WM_POINTERDOWN: - windows.SetCapture(w.hwnd) - case windows.WM_POINTERUP: - windows.ReleaseCapture() - } - - kind := pointer.Move - switch pi.ButtonChangeType { - case windows.POINTER_CHANGE_FIRSTBUTTON_DOWN, windows.POINTER_CHANGE_SECONDBUTTON_DOWN, windows.POINTER_CHANGE_THIRDBUTTON_DOWN, windows.POINTER_CHANGE_FOURTHBUTTON_DOWN, windows.POINTER_CHANGE_FIFTHBUTTON_DOWN: - kind = pointer.Press - case windows.POINTER_CHANGE_FIRSTBUTTON_UP, windows.POINTER_CHANGE_SECONDBUTTON_UP, windows.POINTER_CHANGE_THIRDBUTTON_UP, windows.POINTER_CHANGE_FOURTHBUTTON_UP, windows.POINTER_CHANGE_FIFTHBUTTON_UP: - kind = pointer.Release - } - - if (pi.PointerFlags&windows.POINTER_FLAG_CANCELED != 0) || (msg == windows.WM_POINTERCAPTURECHANGED) { - kind = pointer.Cancel - } - - w.pointerUpdate(pi, pid, kind, lParam) - case windows.WM_CANCELMODE: - w.ProcessEvent(pointer.Event{ - Kind: pointer.Cancel, - }) - case windows.WM_SETFOCUS: - w.config.Focused = true - w.ProcessEvent(ConfigEvent{Config: w.config}) - case windows.WM_KILLFOCUS: - w.config.Focused = false - w.ProcessEvent(ConfigEvent{Config: w.config}) - case windows.WM_NCHITTEST: - if w.config.Decorated { - // Let the system handle it. - break - } - x, y := coordsFromlParam(lParam) - np := windows.Point{X: int32(x), Y: int32(y)} - windows.ScreenToClient(w.hwnd, &np) - return w.hitTest(int(np.X), int(np.Y)) - case windows.WM_POINTERWHEEL: - w.scrollEvent(wParam, lParam, false, getModifiers()) - case windows.WM_POINTERHWHEEL: - w.scrollEvent(wParam, lParam, true, getModifiers()) - case windows.WM_DESTROY: - w.ProcessEvent(Win32ViewEvent{}) - w.ProcessEvent(DestroyEvent{}) - w.w = nil - if w.hdc != 0 { - windows.ReleaseDC(w.hdc) - w.hdc = 0 - } - // The system destroys the HWND for us. - w.hwnd = 0 - windows.PostQuitMessage(0) - return 0 - case windows.WM_NCCALCSIZE: - if w.config.Decorated { - // Let Windows handle decorations. - break - } - // No client areas; we draw decorations ourselves. - if wParam != 1 { - return 0 - } - // lParam contains an NCCALCSIZE_PARAMS for us to adjust. - place := windows.GetWindowPlacement(w.hwnd) - if !place.IsMaximized() { - // Nothing do adjust. - return 0 - } - // Adjust window position to avoid the extra padding in maximized - // state. See https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543. - // Note that trying to do the adjustment in WM_GETMINMAXINFO is ignored by Windows. - szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(lParam)) - mi := windows.GetMonitorInfo(w.hwnd) - szp.Rgrc[0] = mi.WorkArea - return 0 - case windows.WM_PAINT: - w.draw(true) - case windows.WM_STYLECHANGED: - w.update() - case windows.WM_WINDOWPOSCHANGED: - w.update() - case windows.WM_SIZE: - w.update() - case windows.WM_GETMINMAXINFO: - mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam)) - - var frameDims image.Point - if w.config.Decorated { - frameDims = w.frameDims - } - if p := w.config.MinSize; p.X > 0 || p.Y > 0 { - p = p.Add(frameDims) - mm.PtMinTrackSize = windows.Point{ - X: int32(p.X), - Y: int32(p.Y), - } - } - if p := w.config.MaxSize; p.X > 0 || p.Y > 0 { - p = p.Add(frameDims) - mm.PtMaxTrackSize = windows.Point{ - X: int32(p.X), - Y: int32(p.Y), - } - } - return 0 - case windows.WM_SETCURSOR: - w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT - if w.cursorIn { - windows.SetCursor(w.cursor) - return windows.TRUE - } - case _WM_WAKEUP: - w.loop.Wakeup() - w.loop.FlushEvents() - case windows.WM_IME_STARTCOMPOSITION: - imc := windows.ImmGetContext(w.hwnd) - if imc == 0 { - return windows.TRUE - } - defer windows.ImmReleaseContext(w.hwnd, imc) - sel := w.w.EditorState().Selection - caret := sel.Transform.Transform(sel.Caret.Pos.Add(f32.Pt(0, sel.Caret.Descent))) - icaret := image.Pt(int(caret.X+.5), int(caret.Y+.5)) - windows.ImmSetCompositionWindow(imc, icaret.X, icaret.Y) - windows.ImmSetCandidateWindow(imc, icaret.X, icaret.Y) - case windows.WM_IME_COMPOSITION: - imc := windows.ImmGetContext(w.hwnd) - if imc == 0 { - return windows.TRUE - } - defer windows.ImmReleaseContext(w.hwnd, imc) - state := w.w.EditorState() - rng := state.compose - if rng.Start == -1 { - rng = state.Selection.Range - } - if rng.Start > rng.End { - rng.Start, rng.End = rng.End, rng.Start - } - var replacement string - switch { - case lParam&windows.GCS_RESULTSTR != 0: - replacement = windows.ImmGetCompositionString(imc, windows.GCS_RESULTSTR) - case lParam&windows.GCS_COMPSTR != 0: - replacement = windows.ImmGetCompositionString(imc, windows.GCS_COMPSTR) - } - end := rng.Start + utf8.RuneCountInString(replacement) - w.w.EditorReplace(rng, replacement) - state = w.w.EditorState() - comp := key.Range{ - Start: rng.Start, - End: end, - } - if lParam&windows.GCS_DELTASTART != 0 { - start := windows.ImmGetCompositionValue(imc, windows.GCS_DELTASTART) - comp.Start = state.RunesIndex(state.UTF16Index(comp.Start) + start) - } - w.w.SetComposingRegion(comp) - pos := end - if lParam&windows.GCS_CURSORPOS != 0 { - rel := windows.ImmGetCompositionValue(imc, windows.GCS_CURSORPOS) - pos = state.RunesIndex(state.UTF16Index(rng.Start) + rel) - } - w.w.SetEditorSelection(key.Range{Start: pos, End: pos}) - return windows.TRUE - case windows.WM_IME_ENDCOMPOSITION: - w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) - return windows.TRUE - } - - return windows.DefWindowProc(hwnd, msg, wParam, lParam) -} - -func getModifiers() key.Modifiers { - var kmods key.Modifiers - if windows.GetKeyState(windows.VK_LWIN)&0x1000 != 0 || windows.GetKeyState(windows.VK_RWIN)&0x1000 != 0 { - kmods |= key.ModSuper - } - if windows.GetKeyState(windows.VK_MENU)&0x1000 != 0 { - kmods |= key.ModAlt - } - if windows.GetKeyState(windows.VK_CONTROL)&0x1000 != 0 { - kmods |= key.ModCtrl - } - if windows.GetKeyState(windows.VK_SHIFT)&0x1000 != 0 { - kmods |= key.ModShift - } - return kmods -} - -// hitTest returns the non-client area hit by the point, needed to -// process WM_NCHITTEST. -func (w *window) hitTest(x, y int) uintptr { - if w.config.Mode != Windowed { - // Only windowed mode should allow resizing. - return windows.HTCLIENT - } - // Check for resize handle before system actions; otherwise it can be impossible to - // resize a custom-decorations window when the system move area is flush with the - // edge of the window. - top := y <= w.borderSize.Y - bottom := y >= w.config.Size.Y-w.borderSize.Y - left := x <= w.borderSize.X - right := x >= w.config.Size.X-w.borderSize.X - switch { - case top && left: - return windows.HTTOPLEFT - case top && right: - return windows.HTTOPRIGHT - case bottom && left: - return windows.HTBOTTOMLEFT - case bottom && right: - return windows.HTBOTTOMRIGHT - case top: - return windows.HTTOP - case bottom: - return windows.HTBOTTOM - case left: - return windows.HTLEFT - case right: - return windows.HTRIGHT - } - p := f32.Pt(float32(x), float32(y)) - if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove { - return windows.HTCAPTION - } - return windows.HTCLIENT -} - -func (w *window) pointerUpdate(pi windows.PointerInfo, pid pointer.ID, kind pointer.Kind, lParam uintptr) { - if !w.config.Focused { - windows.SetFocus(w.hwnd) - } - - src := pointer.Touch - if pi.PointerType == windows.PT_MOUSE { - src = pointer.Mouse - } - - x, y := coordsFromlParam(lParam) - np := windows.Point{X: int32(x), Y: int32(y)} - windows.ScreenToClient(w.hwnd, &np) - p := f32.Point{X: float32(np.X), Y: float32(np.Y)} - w.ProcessEvent(pointer.Event{ - Kind: kind, - Source: src, - Position: p, - PointerID: pid, - Buttons: getPointerButtons(pi), - Time: windows.GetMessageTime(), - Modifiers: getModifiers(), - }) -} - -func coordsFromlParam(lParam uintptr) (int, int) { - x := int(int16(lParam & 0xffff)) - y := int(int16((lParam >> 16) & 0xffff)) - return x, y -} - -func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.Modifiers) { - pid := getPointerIDwParam(wParam) - pi, err := windows.GetPointerInfo(uint32(pid)) - if err != nil { - panic(err) - } - - x, y := coordsFromlParam(lParam) - // The WM_MOUSEWHEEL coordinates are in screen coordinates, in contrast - // to other mouse events. - np := windows.Point{X: int32(x), Y: int32(y)} - windows.ScreenToClient(w.hwnd, &np) - p := f32.Point{X: float32(np.X), Y: float32(np.Y)} - dist := float32(int16(wParam >> 16)) - var sp f32.Point - if horizontal { - sp.X = dist - } else { - // support horizontal scroll (shift + mousewheel) - if kmods == key.ModShift { - sp.X = -dist - } else { - sp.Y = -dist - } - } - w.ProcessEvent(pointer.Event{ - Kind: pointer.Scroll, - Source: pointer.Mouse, - Position: p, - Buttons: getPointerButtons(pi), - Scroll: sp, - Modifiers: kmods, - Time: windows.GetMessageTime(), - }) -} - -// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/ -func (w *window) runLoop() { - msg := new(windows.Msg) -loop: - for { - anim := w.animating - p := windows.GetWindowPlacement(w.hwnd) - if anim && !p.IsMinimized() && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) { - w.draw(false) - continue - } - switch ret := windows.GetMessage(msg, 0, 0, 0); ret { - case -1: - panic(errors.New("GetMessage failed")) - case 0: - // WM_QUIT received. - break loop - } - windows.TranslateMessage(msg) - windows.DispatchMessage(msg) - } -} - -func (w *window) EditorStateChanged(old, new editorState) { - imc := windows.ImmGetContext(w.hwnd) - if imc == 0 { - return - } - defer windows.ImmReleaseContext(w.hwnd, imc) - if old.Selection.Range != new.Selection.Range || old.Snippet != new.Snippet { - windows.ImmNotifyIME(imc, windows.NI_COMPOSITIONSTR, windows.CPS_CANCEL, 0) - } -} - -func (w *window) SetAnimating(anim bool) { - w.animating = anim -} - -func (w *window) ProcessEvent(e event.Event) { - w.w.ProcessEvent(e) - w.loop.FlushEvents() -} - -func (w *window) Event() event.Event { - return w.loop.Event() -} - -func (w *window) Invalidate() { - w.loop.Invalidate() -} - -func (w *window) Run(f func()) { - w.loop.Run(f) -} - -func (w *window) Frame(frame *op.Ops) { - w.loop.Frame(frame) -} - -func (w *window) wakeup() { - if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil { - panic(err) - } -} - -func (w *window) draw(sync bool) { - if w.config.Size.X == 0 || w.config.Size.Y == 0 { - return - } - dpi := windows.GetWindowDPI(w.hwnd) - cfg := configForDPI(dpi) - w.ProcessEvent(frameEvent{ - FrameEvent: FrameEvent{ - Now: time.Now(), - Size: w.config.Size, - Metric: cfg, - }, - Sync: sync, - }) -} - -func (w *window) NewContext() (context, error) { - sort.Slice(drivers, func(i, j int) bool { - return drivers[i].priority < drivers[j].priority - }) - var errs []string - for _, b := range drivers { - ctx, err := b.initializer(w) - if err == nil { - return ctx, nil - } - errs = append(errs, err.Error()) - } - if len(errs) > 0 { - return nil, fmt.Errorf("NewContext: failed to create a GPU device, tried: %s", strings.Join(errs, ", ")) - } - return nil, errors.New("NewContext: no available GPU drivers") -} - -func (w *window) ReadClipboard() { - w.readClipboard() -} - -func (w *window) readClipboard() error { - if err := windows.OpenClipboard(w.hwnd); err != nil { - return err - } - defer windows.CloseClipboard() - mem, err := windows.GetClipboardData(windows.CF_UNICODETEXT) - if err != nil { - return err - } - ptr, err := windows.GlobalLock(mem) - if err != nil { - return err - } - defer windows.GlobalUnlock(mem) - content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) - w.ProcessEvent(transfer.DataEvent{ - Type: "application/text", - Open: func() io.ReadCloser { - return io.NopCloser(strings.NewReader(content)) - }, - }) - return nil -} - -func (w *window) Configure(options []Option) { - dpi := windows.GetSystemDPI() - metric := configForDPI(dpi) - cnf := w.config - cnf.apply(metric, options) - w.config.Title = cnf.Title - w.config.Decorated = cnf.Decorated - w.config.MinSize = cnf.MinSize - w.config.MaxSize = cnf.MaxSize - windows.SetWindowText(w.hwnd, cnf.Title) - - style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE) - var showMode int32 - var x, y, width, height int32 - swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED) - winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW) - style &^= winStyle - switch cnf.Mode { - case Minimized: - style |= winStyle - swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE - showMode = windows.SW_SHOWMINIMIZED - - case Maximized: - style |= winStyle - swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE - showMode = windows.SW_SHOWMAXIMIZED - - case Windowed: - style |= winStyle - showMode = windows.SW_SHOWNORMAL - // Get target for client area size. - width = int32(cnf.Size.X) - height = int32(cnf.Size.Y) - // Get the current window size and position. - wr := windows.GetWindowRect(w.hwnd) - x = wr.Left - y = wr.Top - if cnf.Decorated { - // Compute client size and position. Note that the client size is - // equal to the window size when we are in control of decorations. - r := windows.Rect{ - Right: width, - Bottom: height, - } - windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle) - width = r.Right - r.Left - height = r.Bottom - r.Top - } else { - // Enable drop shadows when we draw decorations. - windows.DwmExtendFrameIntoClientArea(w.hwnd, windows.Margins{-1, -1, -1, -1}) - } - - case Fullscreen: - swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE - showMode = windows.SW_SHOWMAXIMIZED - } - - // Disable window resizing if MinSize and MaxSize are equal. - if cnf.MaxSize != (image.Point{}) && cnf.MinSize == cnf.MaxSize { - style &^= windows.WS_MAXIMIZEBOX - style &^= windows.WS_THICKFRAME - } - - // Note: these invocation all trigger the windows callback method which may process a pending system.ActionCenter - // action, so SetWindowPos should come first so as to not "overwrite" system.ActionCenter. - windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle) - windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style) - windows.ShowWindow(w.hwnd, showMode) -} - -func (w *window) WriteClipboard(mime string, s []byte) { - w.writeClipboard(string(s)) -} - -func (w *window) writeClipboard(s string) error { - if err := windows.OpenClipboard(w.hwnd); err != nil { - return err - } - defer windows.CloseClipboard() - if err := windows.EmptyClipboard(); err != nil { - return err - } - u16, err := gowindows.UTF16FromString(s) - if err != nil { - return err - } - n := len(u16) * int(unsafe.Sizeof(u16[0])) - mem, err := windows.GlobalAlloc(n) - if err != nil { - return err - } - ptr, err := windows.GlobalLock(mem) - if err != nil { - windows.GlobalFree(mem) - return err - } - u16v := unsafe.Slice((*uint16)(ptr), len(u16)) - copy(u16v, u16) - windows.GlobalUnlock(mem) - if err := windows.SetClipboardData(windows.CF_UNICODETEXT, mem); err != nil { - windows.GlobalFree(mem) - return err - } - return nil -} - -func (w *window) SetCursor(cursor pointer.Cursor) { - c, err := loadCursor(cursor) - if err != nil { - c = resources.cursor - } - w.cursor = c - if w.cursorIn { - windows.SetCursor(w.cursor) - } -} - -// windowsCursor contains mapping from pointer.Cursor to an IDC. -var windowsCursor = [...]uint16{ - pointer.CursorDefault: windows.IDC_ARROW, - pointer.CursorNone: 0, - pointer.CursorText: windows.IDC_IBEAM, - pointer.CursorVerticalText: windows.IDC_IBEAM, - pointer.CursorPointer: windows.IDC_HAND, - pointer.CursorCrosshair: windows.IDC_CROSS, - pointer.CursorAllScroll: windows.IDC_SIZEALL, - pointer.CursorColResize: windows.IDC_SIZEWE, - pointer.CursorRowResize: windows.IDC_SIZENS, - pointer.CursorGrab: windows.IDC_SIZEALL, - pointer.CursorGrabbing: windows.IDC_SIZEALL, - pointer.CursorNotAllowed: windows.IDC_NO, - pointer.CursorWait: windows.IDC_WAIT, - pointer.CursorProgress: windows.IDC_APPSTARTING, - pointer.CursorNorthWestResize: windows.IDC_SIZENWSE, - pointer.CursorNorthEastResize: windows.IDC_SIZENESW, - pointer.CursorSouthWestResize: windows.IDC_SIZENESW, - pointer.CursorSouthEastResize: windows.IDC_SIZENWSE, - pointer.CursorNorthSouthResize: windows.IDC_SIZENS, - pointer.CursorEastWestResize: windows.IDC_SIZEWE, - pointer.CursorWestResize: windows.IDC_SIZEWE, - pointer.CursorEastResize: windows.IDC_SIZEWE, - pointer.CursorNorthResize: windows.IDC_SIZENS, - pointer.CursorSouthResize: windows.IDC_SIZENS, - pointer.CursorNorthEastSouthWestResize: windows.IDC_SIZENESW, - pointer.CursorNorthWestSouthEastResize: windows.IDC_SIZENWSE, -} - -func loadCursor(cursor pointer.Cursor) (syscall.Handle, error) { - switch cursor { - case pointer.CursorDefault: - return resources.cursor, nil - case pointer.CursorNone: - return 0, nil - default: - return windows.LoadCursor(windowsCursor[cursor]) - } -} - -func (w *window) ShowTextInput(show bool) {} - -func (w *window) SetInputHint(_ key.InputHint) {} - -func (w *window) HDC() syscall.Handle { - return w.hdc -} - -func (w *window) HWND() (syscall.Handle, int, int) { - return w.hwnd, w.config.Size.X, w.config.Size.Y -} - -func (w *window) Perform(acts system.Action) { - walkActions(acts, func(a system.Action) { - switch a { - case system.ActionCenter: - if w.config.Mode != Windowed { - break - } - r := windows.GetWindowRect(w.hwnd) - dx := r.Right - r.Left - dy := r.Bottom - r.Top - // Calculate center position on current monitor. - mi := windows.GetMonitorInfo(w.hwnd).Monitor - x := (mi.Right - mi.Left - dx) / 2 - y := (mi.Bottom - mi.Top - dy) / 2 - windows.SetWindowPos(w.hwnd, 0, x, y, dx, dy, windows.SWP_NOZORDER|windows.SWP_FRAMECHANGED) - case system.ActionRaise: - w.raise() - case system.ActionClose: - windows.PostMessage(w.hwnd, windows.WM_CLOSE, 0, 0) - } - }) -} - -func (w *window) raise() { - windows.SetForegroundWindow(w.hwnd) - windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST, 0, 0, 0, 0, - windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW) -} - -func convertKeyCode(code uintptr) (key.Name, bool) { - if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' { - return key.Name(rune(code)), true - } - var r key.Name - - switch code { - case windows.VK_ESCAPE: - r = key.NameEscape - case windows.VK_LEFT: - r = key.NameLeftArrow - case windows.VK_RIGHT: - r = key.NameRightArrow - case windows.VK_RETURN: - r = key.NameReturn - case windows.VK_UP: - r = key.NameUpArrow - case windows.VK_DOWN: - r = key.NameDownArrow - case windows.VK_HOME: - r = key.NameHome - case windows.VK_END: - r = key.NameEnd - case windows.VK_BACK: - r = key.NameDeleteBackward - case windows.VK_DELETE: - r = key.NameDeleteForward - case windows.VK_PRIOR: - r = key.NamePageUp - case windows.VK_NEXT: - r = key.NamePageDown - case windows.VK_F1: - r = key.NameF1 - case windows.VK_F2: - r = key.NameF2 - case windows.VK_F3: - r = key.NameF3 - case windows.VK_F4: - r = key.NameF4 - case windows.VK_F5: - r = key.NameF5 - case windows.VK_F6: - r = key.NameF6 - case windows.VK_F7: - r = key.NameF7 - case windows.VK_F8: - r = key.NameF8 - case windows.VK_F9: - r = key.NameF9 - case windows.VK_F10: - r = key.NameF10 - case windows.VK_F11: - r = key.NameF11 - case windows.VK_F12: - r = key.NameF12 - case windows.VK_TAB: - r = key.NameTab - case windows.VK_SPACE: - r = key.NameSpace - case windows.VK_OEM_1: - r = ";" - case windows.VK_OEM_PLUS: - r = "+" - case windows.VK_OEM_COMMA: - r = "," - case windows.VK_OEM_MINUS: - r = "-" - case windows.VK_OEM_PERIOD: - r = "." - case windows.VK_OEM_2: - r = "/" - case windows.VK_OEM_3: - r = "`" - case windows.VK_OEM_4: - r = "[" - case windows.VK_OEM_5, windows.VK_OEM_102: - r = "\\" - case windows.VK_OEM_6: - r = "]" - case windows.VK_OEM_7: - r = "'" - case windows.VK_CONTROL: - r = key.NameCtrl - case windows.VK_SHIFT: - r = key.NameShift - case windows.VK_MENU: - r = key.NameAlt - case windows.VK_LWIN, windows.VK_RWIN: - r = key.NameSuper - default: - return "", false - } - return r, true -} - -func configForDPI(dpi int) unit.Metric { - const inchPrDp = 1.0 / 96.0 - ppdp := float32(dpi) * inchPrDp - return unit.Metric{ - PxPerDp: ppdp, - PxPerSp: ppdp, - } -} - -func (Win32ViewEvent) implementsViewEvent() {} -func (Win32ViewEvent) ImplementsEvent() {} -func (w Win32ViewEvent) Valid() bool { - return w != (Win32ViewEvent{}) -} - -// LOWORD (minwindef.h) -func loWord(val uint32) uint16 { - return uint16(val & 0xFFFF) -} - -// GET_POINTERID_WPARAM (winuser.h) -func getPointerIDwParam(wParam uintptr) pointer.ID { - return pointer.ID(loWord(uint32(wParam))) -} - -func getPointerButtons(pi windows.PointerInfo) pointer.Buttons { - var btns pointer.Buttons - - if pi.PointerFlags&windows.POINTER_FLAG_FIRSTBUTTON != 0 { - btns |= pointer.ButtonPrimary - } else { - btns &^= pointer.ButtonPrimary - } - if pi.PointerFlags&windows.POINTER_FLAG_SECONDBUTTON != 0 { - btns |= pointer.ButtonSecondary - } else { - btns &^= pointer.ButtonSecondary - } - if pi.PointerFlags&windows.POINTER_FLAG_THIRDBUTTON != 0 { - btns |= pointer.ButtonTertiary - } else { - btns &^= pointer.ButtonTertiary - } - if pi.PointerFlags&windows.POINTER_FLAG_FOURTHBUTTON != 0 { - btns |= pointer.ButtonQuaternary - } else { - btns &^= pointer.ButtonQuaternary - } - if pi.PointerFlags&windows.POINTER_FLAG_FIFTHBUTTON != 0 { - btns |= pointer.ButtonQuinary - } else { - btns &^= pointer.ButtonQuinary - } - - return btns -} diff --git a/gio/app/os_x11.go b/gio/app/os_x11.go deleted file mode 100644 index 6a079c1..0000000 --- a/gio/app/os_x11.go +++ /dev/null @@ -1,928 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build ((linux && !android) || freebsd || openbsd) && !nox11 -// +build linux,!android freebsd openbsd -// +build !nox11 - -package app - -/* -#cgo freebsd openbsd CFLAGS: -I/usr/X11R6/include -I/usr/local/include -#cgo freebsd openbsd LDFLAGS: -L/usr/X11R6/lib -L/usr/local/lib -#cgo freebsd openbsd LDFLAGS: -lX11 -lxkbcommon -lxkbcommon-x11 -lX11-xcb -lXcursor -lXfixes -#cgo linux pkg-config: x11 xkbcommon xkbcommon-x11 x11-xcb xcursor xfixes - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -*/ -import "C" - -import ( - "errors" - "fmt" - "image" - "io" - "strconv" - "strings" - "sync" - "time" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" - - syscall "golang.org/x/sys/unix" - - "github.com/p9c/p9/pkg/gel/gio/app/internal/xkb" -) - -const ( - _NET_WM_STATE_REMOVE = 0 - _NET_WM_STATE_ADD = 1 -) - -type x11Window struct { - w *callbacks - x *C.Display - xkb *xkb.Context - xkbEventBase C.int - xw C.Window - - atoms struct { - // "UTF8_STRING". - utf8string C.Atom - // "text/plain;charset=utf-8". - plaintext C.Atom - // "TARGETS" - targets C.Atom - // "CLIPBOARD". - clipboard C.Atom - // "PRIMARY". - primary C.Atom - // "CLIPBOARD_CONTENT", the clipboard destination property. - clipboardContent C.Atom - // "WM_DELETE_WINDOW" - evDelWindow C.Atom - // "ATOM" - atom C.Atom - // "GTK_TEXT_BUFFER_CONTENTS" - gtk_text_buffer_contents C.Atom - // "_NET_WM_NAME" - wmName C.Atom - // "_NET_WM_STATE" - wmState C.Atom - // "_NET_WM_STATE_FULLSCREEN" - wmStateFullscreen C.Atom - // "_NET_ACTIVE_WINDOW" - wmActiveWindow C.Atom - // _NET_WM_STATE_MAXIMIZED_HORZ - wmStateMaximizedHorz C.Atom - // _NET_WM_STATE_MAXIMIZED_VERT - wmStateMaximizedVert C.Atom - } - metric unit.Metric - notify struct { - read, write int - } - - animating bool - - pointerBtns pointer.Buttons - - clipboard struct { - content []byte - } - cursor pointer.Cursor - config Config - - wakeups chan struct{} - handler x11EventHandler - buf [100]byte -} - -var ( - newX11EGLContext func(w *x11Window) (context, error) - newX11VulkanContext func(w *x11Window) (context, error) -) - -// X11 and Vulkan doesn't work reliably on NVIDIA systems. -// See https://gioui.org/issue/347. -const vulkanBuggy = true - -func (w *x11Window) NewContext() (context, error) { - var firstErr error - if f := newX11VulkanContext; f != nil && !vulkanBuggy { - c, err := f(w) - if err == nil { - return c, nil - } - firstErr = err - } - if f := newX11EGLContext; f != nil { - c, err := f(w) - if err == nil { - return c, nil - } - firstErr = err - } - if firstErr != nil { - return nil, firstErr - } - return nil, errors.New("x11: no available GPU backends") -} - -func (w *x11Window) SetAnimating(anim bool) { - w.animating = anim -} - -func (w *x11Window) ReadClipboard() { - C.XDeleteProperty(w.x, w.xw, w.atoms.clipboardContent) - C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime) -} - -func (w *x11Window) WriteClipboard(mime string, s []byte) { - w.clipboard.content = s - C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime) - C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime) -} - -func (w *x11Window) Configure(options []Option) { - var shints C.XSizeHints - prev := w.config - cnf := w.config - cnf.apply(w.metric, options) - // Decorations are never disabled. - cnf.Decorated = true - - switch cnf.Mode { - case Fullscreen: - switch prev.Mode { - case Fullscreen: - case Minimized: - w.raise() - fallthrough - default: - w.config.Mode = Fullscreen - w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateFullscreen, 0) - } - case Minimized: - switch prev.Mode { - case Minimized, Fullscreen: - default: - w.config.Mode = Minimized - screen := C.XDefaultScreen(w.x) - C.XIconifyWindow(w.x, w.xw, screen) - } - case Maximized: - switch prev.Mode { - case Fullscreen: - case Minimized: - w.raise() - fallthrough - default: - w.config.Mode = Maximized - w.sendWMStateEvent(_NET_WM_STATE_ADD, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert) - w.setTitle(prev, cnf) - } - case Windowed: - switch prev.Mode { - case Fullscreen: - w.config.Mode = Windowed - w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateFullscreen, 0) - C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y)) - case Minimized: - w.config.Mode = Windowed - w.raise() - case Maximized: - w.config.Mode = Windowed - w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert) - } - w.setTitle(prev, cnf) - if prev.Size != cnf.Size { - w.config.Size = cnf.Size - C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y)) - } - if prev.MinSize != cnf.MinSize { - w.config.MinSize = cnf.MinSize - shints.min_width = C.int(cnf.MinSize.X) - shints.min_height = C.int(cnf.MinSize.Y) - shints.flags = C.PMinSize - } - if prev.MaxSize != cnf.MaxSize { - w.config.MaxSize = cnf.MaxSize - shints.max_width = C.int(cnf.MaxSize.X) - shints.max_height = C.int(cnf.MaxSize.Y) - shints.flags = shints.flags | C.PMaxSize - } - if shints.flags != 0 { - C.XSetWMNormalHints(w.x, w.xw, &shints) - } - } - if cnf.Decorated != prev.Decorated { - w.config.Decorated = cnf.Decorated - } - w.ProcessEvent(ConfigEvent{Config: w.config}) -} - -func (w *x11Window) setTitle(prev, cnf Config) { - if prev.Title != cnf.Title { - title := cnf.Title - ctitle := C.CString(title) - defer C.free(unsafe.Pointer(ctitle)) - C.XStoreName(w.x, w.xw, ctitle) - // set _NET_WM_NAME as well for UTF-8 support in window title. - C.XSetTextProperty(w.x, w.xw, - &C.XTextProperty{ - value: (*C.uchar)(unsafe.Pointer(ctitle)), - encoding: w.atoms.utf8string, - format: 8, - nitems: C.ulong(len(title)), - }, - w.atoms.wmName) - } -} - -func (w *x11Window) Perform(acts system.Action) { - walkActions(acts, func(a system.Action) { - switch a { - case system.ActionCenter: - w.center() - case system.ActionRaise: - w.raise() - } - }) - if acts&system.ActionClose != 0 { - w.close() - } -} - -func (w *x11Window) center() { - screen := C.XDefaultScreen(w.x) - width := C.XDisplayWidth(w.x, screen) - height := C.XDisplayHeight(w.x, screen) - - var attrs C.XWindowAttributes - C.XGetWindowAttributes(w.x, w.xw, &attrs) - width -= attrs.border_width - height -= attrs.border_width - - sz := w.config.Size - x := (int(width) - sz.X) / 2 - y := (int(height) - sz.Y) / 2 - - C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y)) -} - -func (w *x11Window) raise() { - var xev C.XEvent - ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) - *ev = C.XClientMessageEvent{ - _type: C.ClientMessage, - display: w.x, - window: w.xw, - message_type: w.atoms.wmActiveWindow, - format: 32, - } - C.XSendEvent( - w.x, - C.XDefaultRootWindow(w.x), // MUST be the root window - C.False, - C.SubstructureNotifyMask|C.SubstructureRedirectMask, - &xev, - ) - C.XMapRaised(w.display(), w.xw) -} - -func (w *x11Window) SetCursor(cursor pointer.Cursor) { - if cursor == pointer.CursorNone { - w.cursor = cursor - C.XFixesHideCursor(w.x, w.xw) - return - } - - xcursor := xCursor[cursor] - cname := C.CString(xcursor) - defer C.free(unsafe.Pointer(cname)) - c := C.XcursorLibraryLoadCursor(w.x, cname) - if c == 0 { - cursor = pointer.CursorDefault - } - w.cursor = cursor - // If c if null (i.e. cursor was not found), - // XDefineCursor will use the default cursor. - C.XDefineCursor(w.x, w.xw, c) -} - -func (w *x11Window) ShowTextInput(show bool) {} - -func (w *x11Window) SetInputHint(_ key.InputHint) {} - -func (w *x11Window) EditorStateChanged(old, new editorState) {} - -// close the window. -func (w *x11Window) close() { - var xev C.XEvent - ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) - *ev = C.XClientMessageEvent{ - _type: C.ClientMessage, - display: w.x, - window: w.xw, - message_type: w.atom("WM_PROTOCOLS", true), - format: 32, - } - arr := (*[5]C.long)(unsafe.Pointer(&ev.data)) - arr[0] = C.long(w.atoms.evDelWindow) - arr[1] = C.CurrentTime - C.XSendEvent(w.x, w.xw, C.False, C.NoEventMask, &xev) -} - -// action is one of _NET_WM_STATE_REMOVE, _NET_WM_STATE_ADD. -func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) { - var xev C.XEvent - ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) - *ev = C.XClientMessageEvent{ - _type: C.ClientMessage, - display: w.x, - window: w.xw, - message_type: w.atoms.wmState, - format: 32, - } - data := (*[5]C.long)(unsafe.Pointer(&ev.data)) - data[0] = C.long(action) - data[1] = C.long(atom1) - data[2] = C.long(atom2) - data[3] = 1 // application - - C.XSendEvent( - w.x, - C.XDefaultRootWindow(w.x), // MUST be the root window - C.False, - C.SubstructureNotifyMask|C.SubstructureRedirectMask, - &xev, - ) -} - -var x11OneByte = make([]byte, 1) - -func (w *x11Window) ProcessEvent(e event.Event) { - w.w.ProcessEvent(e) -} - -func (w *x11Window) shutdown(err error) { - w.ProcessEvent(X11ViewEvent{}) - w.ProcessEvent(DestroyEvent{Err: err}) - w.destroy() -} - -func (w *x11Window) Event() event.Event { - for { - evt, ok := w.w.nextEvent() - if !ok { - w.dispatch() - continue - } - return evt - } -} - -func (w *x11Window) Run(f func()) { - f() -} - -func (w *x11Window) Frame(frame *op.Ops) { - w.w.ProcessFrame(frame, nil) -} - -func (w *x11Window) Invalidate() { - select { - case w.wakeups <- struct{}{}: - default: - } - if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN { - panic(fmt.Errorf("failed to write to pipe: %v", err)) - } -} - -func (w *x11Window) display() *C.Display { - return w.x -} - -func (w *x11Window) window() (C.Window, int, int) { - return w.xw, w.config.Size.X, w.config.Size.Y -} - -func (w *x11Window) dispatch() { - if w.x == nil { - // Only Invalidate can wake us up. - <-w.wakeups - w.w.Invalidate() - return - } - - select { - case <-w.wakeups: - w.w.Invalidate() - default: - } - - xfd := C.XConnectionNumber(w.x) - - // Poll for events and notifications. - pollfds := []syscall.PollFd{ - {Fd: int32(xfd), Events: syscall.POLLIN | syscall.POLLERR}, - {Fd: int32(w.notify.read), Events: syscall.POLLIN | syscall.POLLERR}, - } - xEvents := &pollfds[0].Revents - // Plenty of room for a backlog of notifications. - - var syn, anim bool - // Check for pending draw events before checking animation or blocking. - // This fixes an issue on Xephyr where on startup XPending() > 0 but - // poll will still block. This also prevents no-op calls to poll. - syn = w.handler.handleEvents() - if w.x == nil { - // handleEvents received a close request and destroyed the window. - return - } - if !syn { - anim = w.animating - if !anim { - // Clear poll events. - *xEvents = 0 - // Wait for X event or gio notification. - if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { - panic(fmt.Errorf("x11 loop: poll failed: %w", err)) - } - switch { - case *xEvents&syscall.POLLIN != 0: - syn = w.handler.handleEvents() - if w.x == nil { - return - } - case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0: - } - } - } - // Clear notifications. - for { - _, err := syscall.Read(w.notify.read, w.buf[:]) - if err == syscall.EAGAIN { - break - } - if err != nil { - panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err)) - } - } - if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 { - w.ProcessEvent(frameEvent{ - FrameEvent: FrameEvent{ - Now: time.Now(), - Size: w.config.Size, - Metric: w.metric, - }, - Sync: syn, - }) - } -} - -func (w *x11Window) destroy() { - if w.notify.write != 0 { - syscall.Close(w.notify.write) - w.notify.write = 0 - } - if w.notify.read != 0 { - syscall.Close(w.notify.read) - w.notify.read = 0 - } - if w.xkb != nil { - w.xkb.Destroy() - w.xkb = nil - } - C.XDestroyWindow(w.x, w.xw) - C.XCloseDisplay(w.x) - w.x = nil -} - -// atom is a wrapper around XInternAtom. Callers should cache the result -// in order to limit round-trips to the X server. -func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom { - cname := C.CString(name) - defer C.free(unsafe.Pointer(cname)) - flag := C.Bool(C.False) - if onlyIfExists { - flag = C.True - } - return C.XInternAtom(w.x, cname, flag) -} - -// x11EventHandler wraps static variables for the main event loop. -// Its sole purpose is to prevent heap allocation and reduce clutter -// in x11window.loop. -type x11EventHandler struct { - w *x11Window - text []byte - xev *C.XEvent -} - -// handleEvents returns true if the window needs to be redrawn. -func (h *x11EventHandler) handleEvents() bool { - w := h.w - xev := h.xev - redraw := false - for C.XPending(w.x) != 0 { - C.XNextEvent(w.x, xev) - if C.XFilterEvent(xev, C.None) == C.True { - continue - } - switch _type := (*C.XAnyEvent)(unsafe.Pointer(xev))._type; _type { - case h.w.xkbEventBase: - xkbEvent := (*C.XkbAnyEvent)(unsafe.Pointer(xev)) - switch xkbEvent.xkb_type { - case C.XkbNewKeyboardNotify, C.XkbMapNotify: - if err := h.w.updateXkbKeymap(); err != nil { - panic(err) - } - case C.XkbStateNotify: - state := (*C.XkbStateNotifyEvent)(unsafe.Pointer(xev)) - h.w.xkb.UpdateMask(uint32(state.base_mods), uint32(state.latched_mods), uint32(state.locked_mods), - uint32(state.base_group), uint32(state.latched_group), uint32(state.locked_group)) - } - case C.KeyPress, C.KeyRelease: - ks := key.Press - if _type == C.KeyRelease { - ks = key.Release - } - kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev)) - for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode), ks) { - if ee, ok := e.(key.EditEvent); ok { - // There's no support for IME yet. - w.w.EditorInsert(ee.Text) - } else { - w.ProcessEvent(e) - } - } - case C.ButtonPress, C.ButtonRelease: - bevt := (*C.XButtonEvent)(unsafe.Pointer(xev)) - ev := pointer.Event{ - Kind: pointer.Press, - Source: pointer.Mouse, - Position: f32.Point{ - X: float32(bevt.x), - Y: float32(bevt.y), - }, - Time: time.Duration(bevt.time) * time.Millisecond, - Modifiers: w.xkb.Modifiers(), - } - if bevt._type == C.ButtonRelease { - ev.Kind = pointer.Release - } - var btn pointer.Buttons - const scrollScale = 10 - switch bevt.button { - case C.Button1: - btn = pointer.ButtonPrimary - case C.Button2: - btn = pointer.ButtonTertiary - case C.Button3: - btn = pointer.ButtonSecondary - case C.Button4: - ev.Kind = pointer.Scroll - // scroll up or left (if shift is pressed). - if ev.Modifiers == key.ModShift { - ev.Scroll.X = -scrollScale - } else { - ev.Scroll.Y = -scrollScale - } - case C.Button5: - // scroll down or right (if shift is pressed). - ev.Kind = pointer.Scroll - if ev.Modifiers == key.ModShift { - ev.Scroll.X = +scrollScale - } else { - ev.Scroll.Y = +scrollScale - } - case 6: - // http://xahlee.info/linux/linux_x11_mouse_button_number.html - // scroll left. - ev.Kind = pointer.Scroll - ev.Scroll.X = -scrollScale * 2 - case 7: - // scroll right - ev.Kind = pointer.Scroll - ev.Scroll.X = +scrollScale * 2 - default: - continue - } - switch _type { - case C.ButtonPress: - w.pointerBtns |= btn - case C.ButtonRelease: - w.pointerBtns &^= btn - } - ev.Buttons = w.pointerBtns - w.ProcessEvent(ev) - case C.MotionNotify: - mevt := (*C.XMotionEvent)(unsafe.Pointer(xev)) - w.ProcessEvent(pointer.Event{ - Kind: pointer.Move, - Source: pointer.Mouse, - Buttons: w.pointerBtns, - Position: f32.Point{ - X: float32(mevt.x), - Y: float32(mevt.y), - }, - Time: time.Duration(mevt.time) * time.Millisecond, - Modifiers: w.xkb.Modifiers(), - }) - case C.Expose: // update - // redraw only on the last expose event - redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0 - case C.FocusIn: - w.config.Focused = true - w.ProcessEvent(ConfigEvent{Config: w.config}) - case C.FocusOut: - w.config.Focused = false - w.ProcessEvent(ConfigEvent{Config: w.config}) - case C.ConfigureNotify: // window configuration change - cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev)) - if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size { - w.config.Size = sz - w.ProcessEvent(ConfigEvent{Config: w.config}) - } - // redraw will be done by a later expose event - case C.SelectionNotify: - cevt := (*C.XSelectionEvent)(unsafe.Pointer(xev)) - prop := w.atoms.clipboardContent - if cevt.property != prop { - break - } - if cevt.selection != w.atoms.clipboard { - break - } - var text C.XTextProperty - if st := C.XGetTextProperty(w.x, w.xw, &text, prop); st == 0 { - // Failed; ignore. - break - } - if text.format != 8 || text.encoding != w.atoms.utf8string { - // Ignore non-utf-8 encoded strings. - break - } - str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems)) - w.ProcessEvent(transfer.DataEvent{ - Type: "application/text", - Open: func() io.ReadCloser { - return io.NopCloser(strings.NewReader(str)) - }, - }) - case C.SelectionRequest: - cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev)) - if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None { - // Unsupported clipboard or obsolete requestor. - break - } - notify := func() { - var xev C.XEvent - ev := (*C.XSelectionEvent)(unsafe.Pointer(&xev)) - *ev = C.XSelectionEvent{ - _type: C.SelectionNotify, - display: cevt.display, - requestor: cevt.requestor, - selection: cevt.selection, - target: cevt.target, - property: cevt.property, - time: cevt.time, - } - C.XSendEvent(w.x, cevt.requestor, 0, 0, &xev) - } - switch cevt.target { - case w.atoms.targets: - // The requestor wants the supported clipboard - // formats. First write the targets... - formats := [...]C.long{ - C.long(w.atoms.targets), - C.long(w.atoms.utf8string), - C.long(w.atoms.plaintext), - // GTK clients need this. - C.long(w.atoms.gtk_text_buffer_contents), - } - C.XChangeProperty(w.x, cevt.requestor, cevt.property, w.atoms.atom, - 32 /* bitwidth of formats */, C.PropModeReplace, - (*C.uchar)(unsafe.Pointer(&formats)), C.int(len(formats)), - ) - // ...then notify the requestor. - notify() - case w.atoms.plaintext, w.atoms.utf8string, w.atoms.gtk_text_buffer_contents: - content := w.clipboard.content - var ptr *C.uchar - if len(content) > 0 { - ptr = (*C.uchar)(unsafe.Pointer(&content[0])) - } - C.XChangeProperty(w.x, cevt.requestor, cevt.property, cevt.target, - 8 /* bitwidth */, C.PropModeReplace, - ptr, C.int(len(content)), - ) - notify() - } - case C.ClientMessage: // extensions - cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev)) - switch *(*C.long)(unsafe.Pointer(&cevt.data)) { - case C.long(w.atoms.evDelWindow): - w.shutdown(nil) - return false - } - } - } - return redraw -} - -var x11Threads sync.Once - -func init() { - x11Driver = newX11Window -} - -func newX11Window(gioWin *callbacks, options []Option) error { - var err error - - pipe := make([]int, 2) - if err := syscall.Pipe2(pipe, syscall.O_NONBLOCK|syscall.O_CLOEXEC); err != nil { - return fmt.Errorf("NewX11Window: failed to create pipe: %w", err) - } - - x11Threads.Do(func() { - if C.XInitThreads() == 0 { - err = errors.New("x11: threads init failed") - } - C.XrmInitialize() - }) - if err != nil { - return err - } - dpy := C.XOpenDisplay(nil) - if dpy == nil { - return errors.New("x11: cannot connect to the X server") - } - var major, minor C.int = C.XkbMajorVersion, C.XkbMinorVersion - var xkbEventBase C.int - if C.XkbQueryExtension(dpy, nil, &xkbEventBase, nil, &major, &minor) != C.True { - C.XCloseDisplay(dpy) - return errors.New("x11: XkbQueryExtension failed") - } - const bits = C.uint(C.XkbNewKeyboardNotifyMask | C.XkbMapNotifyMask | C.XkbStateNotifyMask) - if C.XkbSelectEvents(dpy, C.XkbUseCoreKbd, bits, bits) != C.True { - C.XCloseDisplay(dpy) - return errors.New("x11: XkbSelectEvents failed") - } - xkb, err := xkb.New() - if err != nil { - C.XCloseDisplay(dpy) - return fmt.Errorf("x11: %v", err) - } - - ppsp := x11DetectUIScale(dpy) - cfg := unit.Metric{PxPerDp: ppsp, PxPerSp: ppsp} - // Only use cnf for getting the window size. - var cnf Config - cnf.apply(cfg, options) - - swa := C.XSetWindowAttributes{ - event_mask: C.ExposureMask | C.FocusChangeMask | // update - C.KeyPressMask | C.KeyReleaseMask | // keyboard - C.ButtonPressMask | C.ButtonReleaseMask | // mouse clicks - C.PointerMotionMask | // mouse movement - C.StructureNotifyMask, // resize - background_pixmap: C.None, - override_redirect: C.False, - } - win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy), - 0, 0, C.uint(cnf.Size.X), C.uint(cnf.Size.Y), - 0, C.CopyFromParent, C.InputOutput, nil, - C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa) - - w := &x11Window{ - w: gioWin, x: dpy, xw: win, - metric: cfg, - xkb: xkb, - xkbEventBase: xkbEventBase, - wakeups: make(chan struct{}, 1), - config: Config{Size: cnf.Size}, - } - w.handler = x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)} - w.notify.read = pipe[0] - w.notify.write = pipe[1] - w.w.SetDriver(w) - - if err := w.updateXkbKeymap(); err != nil { - w.destroy() - return err - } - - var hints C.XWMHints - hints.input = C.True - hints.flags = C.InputHint - C.XSetWMHints(dpy, win, &hints) - - name := C.CString(ID) - defer C.free(unsafe.Pointer(name)) - wmhints := C.XClassHint{name, name} - C.XSetClassHint(dpy, win, &wmhints) - - w.atoms.utf8string = w.atom("UTF8_STRING", false) - w.atoms.plaintext = w.atom("text/plain;charset=utf-8", false) - w.atoms.gtk_text_buffer_contents = w.atom("GTK_TEXT_BUFFER_CONTENTS", false) - w.atoms.evDelWindow = w.atom("WM_DELETE_WINDOW", false) - w.atoms.clipboard = w.atom("CLIPBOARD", false) - w.atoms.primary = w.atom("PRIMARY", false) - w.atoms.clipboardContent = w.atom("CLIPBOARD_CONTENT", false) - w.atoms.atom = w.atom("ATOM", false) - w.atoms.targets = w.atom("TARGETS", false) - w.atoms.wmName = w.atom("_NET_WM_NAME", false) - w.atoms.wmState = w.atom("_NET_WM_STATE", false) - w.atoms.wmStateFullscreen = w.atom("_NET_WM_STATE_FULLSCREEN", false) - w.atoms.wmActiveWindow = w.atom("_NET_ACTIVE_WINDOW", false) - w.atoms.wmStateMaximizedHorz = w.atom("_NET_WM_STATE_MAXIMIZED_HORZ", false) - w.atoms.wmStateMaximizedVert = w.atom("_NET_WM_STATE_MAXIMIZED_VERT", false) - - // extensions - C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1) - - // make the window visible on the screen - C.XMapWindow(dpy, win) - w.Configure(options) - w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)}) - return nil -} - -// detectUIScale reports the system UI scale, or 1.0 if it fails. -func x11DetectUIScale(dpy *C.Display) float32 { - // default fixed DPI value used in most desktop UI toolkits - const defaultDesktopDPI = 96 - var scale float32 = 1.0 - - // Get actual DPI from X resource Xft.dpi (set by GTK and Qt). - // This value is entirely based on user preferences and conflates both - // screen (UI) scaling and font scale. - rms := C.XResourceManagerString(dpy) - if rms != nil { - db := C.XrmGetStringDatabase(rms) - if db != nil { - var ( - t *C.char - v C.XrmValue - ) - if C.XrmGetResource(db, (*C.char)(unsafe.Pointer(&[]byte("Xft.dpi\x00")[0])), - (*C.char)(unsafe.Pointer(&[]byte("Xft.Dpi\x00")[0])), &t, &v) != C.False { - if t != nil && C.GoString(t) == "String" { - f, err := strconv.ParseFloat(C.GoString(v.addr), 32) - if err == nil { - scale = float32(f) / defaultDesktopDPI - } - } - } - C.XrmDestroyDatabase(db) - } - } - - return scale -} - -func (w *x11Window) updateXkbKeymap() error { - w.xkb.DestroyKeymapState() - ctx := (*C.struct_xkb_context)(unsafe.Pointer(w.xkb.Ctx)) - xcb := C.XGetXCBConnection(w.x) - if xcb == nil { - return errors.New("x11: XGetXCBConnection failed") - } - xkbDevID := C.xkb_x11_get_core_keyboard_device_id(xcb) - if xkbDevID == -1 { - return errors.New("x11: xkb_x11_get_core_keyboard_device_id failed") - } - keymap := C.xkb_x11_keymap_new_from_device(ctx, xcb, xkbDevID, C.XKB_KEYMAP_COMPILE_NO_FLAGS) - if keymap == nil { - return errors.New("x11: xkb_x11_keymap_new_from_device failed") - } - state := C.xkb_x11_state_new_from_device(keymap, xcb, xkbDevID) - if state == nil { - C.xkb_keymap_unref(keymap) - return errors.New("x11: xkb_x11_keymap_new_from_device failed") - } - w.xkb.SetKeymap(unsafe.Pointer(keymap), unsafe.Pointer(state)) - return nil -} diff --git a/gio/app/permission/bluetooth/main.go b/gio/app/permission/bluetooth/main.go deleted file mode 100644 index 0997a33..0000000 --- a/gio/app/permission/bluetooth/main.go +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package bluetooth implements permissions to access Bluetooth and Bluetooth -Low Energy hardware, including the ability to discover and pair devices. - -# Android - -The following entries will be added to AndroidManifest.xml: - - - - - - - -Note that ACCESS_FINE_LOCATION is required on Android before the Bluetooth -device may be used. -See https://developer.android.com/guide/topics/connectivity/bluetooth. - -ACCESS_FINE_LOCATION is a "dangerous" permission. See documentation for -package gioui.org/app/permission for more information. -*/ -package bluetooth diff --git a/gio/app/permission/camera/main.go b/gio/app/permission/camera/main.go deleted file mode 100644 index 87f294c..0000000 --- a/gio/app/permission/camera/main.go +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package camera implements permissions to access camera hardware. - -# Android - -The following entries will be added to AndroidManifest.xml: - - - - -CAMERA is a "dangerous" permission. See documentation for package -gioui.org/app/permission for more information. -*/ -package camera diff --git a/gio/app/permission/doc.go b/gio/app/permission/doc.go deleted file mode 100644 index 8fd22fb..0000000 --- a/gio/app/permission/doc.go +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package permission includes sub-packages that should be imported -by a Gio program or by one of its dependencies to indicate that specific -operating-system permissions are required. For example, if a Gio -program requires access to a device's Bluetooth interface, it -should import "github.com/p9c/p9/pkg/gel/gio/app/permission/bluetooth" as follows: - - package main - - import ( - "github.com/p9c/p9/pkg/gel/gio/app" - _ "github.com/p9c/p9/pkg/gel/gio/app/permission/bluetooth" - ) - - func main() { - ... - } - -Since there are no exported identifiers in the app/permission/bluetooth -package, the import uses the anonymous identifier (_) as the imported -package name. - -As a special case, the gogio tool detects when a program directly or -indirectly depends on the "net" package from the Go standard library as an -indication that the program requires network access permissions. If a program -requires network permissions but does not directly or indirectly import -"net", it will be necessary to add the following code somewhere in the -program's source code: - - import ( - ... - _ "net" - ) - -# Android -- Dangerous Permissions - -Certain permissions on Android are marked with a protection level of -"dangerous". This means that, in addition to including the relevant -Gio permission packages, your app will need to prompt the user -specifically to request access. To access the Android Activity -required for prompting, use app.ViewEvent (only available on Android). -app.ViewEvent exposes the underlying Android View, on which the -getContext method returns the Activity. - -For more information on dangerous permissions, see: -https://developer.android.com/guide/topics/permissions/overview#dangerous_permissions -*/ -package permission diff --git a/gio/app/permission/networkstate/main.go b/gio/app/permission/networkstate/main.go deleted file mode 100644 index b32559e..0000000 --- a/gio/app/permission/networkstate/main.go +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package networkstate implements permissions to access network connectivity information. - -# Android - -The following entries will be added to AndroidManifest.xml: - - -*/ -package networkstate diff --git a/gio/app/permission/storage/main.go b/gio/app/permission/storage/main.go deleted file mode 100644 index 87a9e3e..0000000 --- a/gio/app/permission/storage/main.go +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package storage implements read and write storage permissions -on mobile devices. - -# Android - -The following entries will be added to AndroidManifest.xml: - - - - -READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE are "dangerous" permissions. -See documentation for package gioui.org/app/permission for more information. -*/ -package storage diff --git a/gio/app/permission/wakelock/wakelock.go b/gio/app/permission/wakelock/wakelock.go deleted file mode 100644 index 26ee7fb..0000000 --- a/gio/app/permission/wakelock/wakelock.go +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package wakelock implements permission to acquire locks that keep the system -from suspending. - -# Android - -The following entries will be added to AndroidManifest.xml: - - -*/ -package wakelock diff --git a/gio/app/runmain.go b/gio/app/runmain.go deleted file mode 100644 index a1c1e3d..0000000 --- a/gio/app/runmain.go +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build android || (darwin && ios) -// +build android darwin,ios - -package app - -// Android only supports non-Java programs as c-shared libraries. -// Unfortunately, Go does not run a program's main function in -// library mode. To make Gio programs simpler and uniform, we'll -// link to the main function here and call it from Java. - -import ( - "sync" - _ "unsafe" // for go:linkname -) - -//go:linkname mainMain main.main -func mainMain() - -var runMainOnce sync.Once - -func runMain() { - runMainOnce.Do(func() { - // Indirect call, since the linker does not know the address of main when - // laying down this package. - fn := mainMain - fn() - }) -} diff --git a/gio/app/system.go b/gio/app/system.go deleted file mode 100644 index 2c9acc9..0000000 --- a/gio/app/system.go +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -// DestroyEvent is the last event sent through -// a window event channel. -type DestroyEvent struct { - // Err is nil for normal window closures. If a - // window is prematurely closed, Err is the cause. - Err error -} - -func (DestroyEvent) ImplementsEvent() {} diff --git a/gio/app/vulkan.go b/gio/app/vulkan.go deleted file mode 100644 index 701e54e..0000000 --- a/gio/app/vulkan.go +++ /dev/null @@ -1,218 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build (linux || freebsd) && !novulkan -// +build linux freebsd -// +build !novulkan - -package app - -import ( - "errors" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/vk" -) - -type vkContext struct { - physDev vk.PhysicalDevice - inst vk.Instance - dev vk.Device - queueFam int - queue vk.Queue - acquireSem vk.Semaphore - presentSem vk.Semaphore - fence vk.Fence - - swchain vk.Swapchain - imgs []vk.Image - views []vk.ImageView - fbos []vk.Framebuffer - format vk.Format - presentIdx int -} - -func newVulkanContext(inst vk.Instance, surf vk.Surface) (*vkContext, error) { - physDev, qFam, err := vk.ChoosePhysicalDevice(inst, surf) - if err != nil { - return nil, err - } - dev, err := vk.CreateDeviceAndQueue(physDev, qFam, "VK_KHR_swapchain") - if err != nil { - return nil, err - } - acquireSem, err := vk.CreateSemaphore(dev) - if err != nil { - vk.DestroyDevice(dev) - return nil, err - } - presentSem, err := vk.CreateSemaphore(dev) - if err != nil { - vk.DestroySemaphore(dev, acquireSem) - vk.DestroyDevice(dev) - return nil, err - } - fence, err := vk.CreateFence(dev, vk.FENCE_CREATE_SIGNALED_BIT) - if err != nil { - vk.DestroySemaphore(dev, presentSem) - vk.DestroySemaphore(dev, acquireSem) - vk.DestroyDevice(dev) - return nil, err - } - c := &vkContext{ - physDev: physDev, - inst: inst, - dev: dev, - queueFam: qFam, - queue: vk.GetDeviceQueue(dev, qFam, 0), - acquireSem: acquireSem, - presentSem: presentSem, - fence: fence, - } - return c, nil -} - -func (c *vkContext) RenderTarget() (gpu.RenderTarget, error) { - vk.WaitForFences(c.dev, c.fence) - vk.ResetFences(c.dev, c.fence) - - imgIdx, err := vk.AcquireNextImage(c.dev, c.swchain, c.acquireSem, 0) - if err := mapSurfaceErr(err); err != nil { - return nil, err - } - c.presentIdx = imgIdx - return gpu.VulkanRenderTarget{ - WaitSem: uint64(c.acquireSem), - SignalSem: uint64(c.presentSem), - Fence: uint64(c.fence), - Framebuffer: uint64(c.fbos[imgIdx]), - Image: uint64(c.imgs[imgIdx]), - }, nil -} - -func (c *vkContext) api() gpu.API { - return gpu.Vulkan{ - PhysDevice: unsafe.Pointer(c.physDev), - Device: unsafe.Pointer(c.dev), - Format: int(c.format), - QueueFamily: c.queueFam, - QueueIndex: 0, - } -} - -func mapErr(err error) error { - var vkErr vk.Error - if errors.As(err, &vkErr) && vkErr == vk.ERROR_DEVICE_LOST { - return gpu.ErrDeviceLost - } - return err -} - -func mapSurfaceErr(err error) error { - var vkErr vk.Error - if !errors.As(err, &vkErr) { - return err - } - switch { - case vkErr == vk.SUBOPTIMAL_KHR: - // Android reports VK_SUBOPTIMAL_KHR when presenting to a rotated - // swapchain (preTransform != currentTransform). However, we don't - // support transforming the output ourselves, so we'll live with it. - return nil - case vkErr == vk.ERROR_OUT_OF_DATE_KHR: - return errOutOfDate - case vkErr == vk.ERROR_SURFACE_LOST_KHR: - // Treating a lost surface as a lost device isn't accurate, but - // probably not worth optimizing. - return gpu.ErrDeviceLost - } - return mapErr(err) -} - -func (c *vkContext) release() { - vk.DeviceWaitIdle(c.dev) - - c.destroySwapchain() - vk.DestroyFence(c.dev, c.fence) - vk.DestroySemaphore(c.dev, c.acquireSem) - vk.DestroySemaphore(c.dev, c.presentSem) - vk.DestroyDevice(c.dev) - *c = vkContext{} -} - -func (c *vkContext) present() error { - return mapSurfaceErr(vk.PresentQueue(c.queue, c.swchain, c.presentSem, c.presentIdx)) -} - -func (c *vkContext) destroyImageViews() { - for _, f := range c.fbos { - vk.DestroyFramebuffer(c.dev, f) - } - c.fbos = nil - for _, view := range c.views { - vk.DestroyImageView(c.dev, view) - } - c.views = nil -} - -func (c *vkContext) destroySwapchain() { - vk.DeviceWaitIdle(c.dev) - - c.destroyImageViews() - if c.swchain != 0 { - vk.DestroySwapchain(c.dev, c.swchain) - c.swchain = 0 - } -} - -func (c *vkContext) refresh(surf vk.Surface, width, height int) error { - vk.DeviceWaitIdle(c.dev) - - c.destroyImageViews() - // Check whether size is valid. That's needed on X11, where ConfigureNotify - // is not always synchronized with the window extent. - caps, err := vk.GetPhysicalDeviceSurfaceCapabilities(c.physDev, surf) - if err != nil { - return err - } - minExt, maxExt := vk.SurfaceCapabilitiesMinExtent(caps), vk.SurfaceCapabilitiesMaxExtent(caps) - if width < minExt.X || maxExt.X < width || height < minExt.Y || maxExt.Y < height { - return errOutOfDate - } - swchain, imgs, format, err := vk.CreateSwapchain(c.physDev, c.dev, surf, width, height, c.swchain) - if c.swchain != 0 { - vk.DestroySwapchain(c.dev, c.swchain) - c.swchain = 0 - } - if err := mapSurfaceErr(err); err != nil { - return err - } - c.swchain = swchain - c.imgs = imgs - c.format = format - pass, err := vk.CreateRenderPass( - c.dev, - format, - vk.ATTACHMENT_LOAD_OP_CLEAR, - vk.IMAGE_LAYOUT_UNDEFINED, - vk.IMAGE_LAYOUT_PRESENT_SRC_KHR, - nil, - ) - if err := mapErr(err); err != nil { - return err - } - defer vk.DestroyRenderPass(c.dev, pass) - for _, img := range imgs { - view, err := vk.CreateImageView(c.dev, img, format) - if err := mapErr(err); err != nil { - return err - } - c.views = append(c.views, view) - fbo, err := vk.CreateFramebuffer(c.dev, pass, view, width, height) - if err := mapErr(err); err != nil { - return err - } - c.fbos = append(c.fbos, fbo) - } - return nil -} diff --git a/gio/app/vulkan_android.go b/gio/app/vulkan_android.go deleted file mode 100644 index 2af9dfa..0000000 --- a/gio/app/vulkan_android.go +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build !novulkan -// +build !novulkan - -package app - -import ( - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/vk" -) - -type wlVkContext struct { - win *window - inst vk.Instance - surf vk.Surface - ctx *vkContext -} - -func init() { - newAndroidVulkanContext = func(w *window) (context, error) { - inst, err := vk.CreateInstance("VK_KHR_surface", "VK_KHR_android_surface") - if err != nil { - return nil, err - } - window, _, _ := w.nativeWindow() - surf, err := vk.CreateAndroidSurface(inst, unsafe.Pointer(window)) - if err != nil { - vk.DestroyInstance(inst) - return nil, err - } - ctx, err := newVulkanContext(inst, surf) - if err != nil { - vk.DestroySurface(inst, surf) - vk.DestroyInstance(inst) - return nil, err - } - c := &wlVkContext{ - win: w, - inst: inst, - surf: surf, - ctx: ctx, - } - return c, nil - } -} - -func (c *wlVkContext) RenderTarget() (gpu.RenderTarget, error) { - return c.ctx.RenderTarget() -} - -func (c *wlVkContext) API() gpu.API { - return c.ctx.api() -} - -func (c *wlVkContext) Release() { - c.ctx.release() - if c.surf != 0 { - vk.DestroySurface(c.inst, c.surf) - } - vk.DestroyInstance(c.inst) - *c = wlVkContext{} -} - -func (c *wlVkContext) Present() error { - return c.ctx.present() -} - -func (c *wlVkContext) Lock() error { - return nil -} - -func (c *wlVkContext) Unlock() {} - -func (c *wlVkContext) Refresh() error { - win, w, h := c.win.nativeWindow() - if c.surf != 0 { - c.ctx.destroySwapchain() - vk.DestroySurface(c.inst, c.surf) - c.surf = 0 - } - surf, err := vk.CreateAndroidSurface(c.inst, unsafe.Pointer(win)) - if err != nil { - return err - } - c.surf = surf - return c.ctx.refresh(c.surf, w, h) -} diff --git a/gio/app/vulkan_wayland.go b/gio/app/vulkan_wayland.go deleted file mode 100644 index 65127b3..0000000 --- a/gio/app/vulkan_wayland.go +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build ((linux && !android) || freebsd) && !nowayland && !novulkan -// +build linux,!android freebsd -// +build !nowayland -// +build !novulkan - -package app - -import ( - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/vk" -) - -type wlVkContext struct { - win *window - inst vk.Instance - surf vk.Surface - ctx *vkContext -} - -func init() { - newWaylandVulkanContext = func(w *window) (context, error) { - inst, err := vk.CreateInstance("VK_KHR_surface", "VK_KHR_wayland_surface") - if err != nil { - return nil, err - } - disp := w.display() - wlSurf, _, _ := w.surface() - surf, err := vk.CreateWaylandSurface(inst, unsafe.Pointer(disp), unsafe.Pointer(wlSurf)) - if err != nil { - vk.DestroyInstance(inst) - return nil, err - } - ctx, err := newVulkanContext(inst, surf) - if err != nil { - vk.DestroySurface(inst, surf) - vk.DestroyInstance(inst) - return nil, err - } - c := &wlVkContext{ - win: w, - inst: inst, - surf: surf, - ctx: ctx, - } - return c, nil - } -} - -func (c *wlVkContext) RenderTarget() (gpu.RenderTarget, error) { - return c.ctx.RenderTarget() -} - -func (c *wlVkContext) API() gpu.API { - return c.ctx.api() -} - -func (c *wlVkContext) Release() { - c.ctx.release() - vk.DestroySurface(c.inst, c.surf) - vk.DestroyInstance(c.inst) - *c = wlVkContext{} -} - -func (c *wlVkContext) Present() error { - return c.ctx.present() -} - -func (c *wlVkContext) Lock() error { - return nil -} - -func (c *wlVkContext) Unlock() {} - -func (c *wlVkContext) Refresh() error { - _, w, h := c.win.surface() - return c.ctx.refresh(c.surf, w, h) -} diff --git a/gio/app/vulkan_x11.go b/gio/app/vulkan_x11.go deleted file mode 100644 index 35bac1a..0000000 --- a/gio/app/vulkan_x11.go +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build ((linux && !android) || freebsd) && !nox11 && !novulkan -// +build linux,!android freebsd -// +build !nox11 -// +build !novulkan - -package app - -import ( - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/vk" -) - -type x11VkContext struct { - win *x11Window - inst vk.Instance - surf vk.Surface - ctx *vkContext -} - -func init() { - newX11VulkanContext = func(w *x11Window) (context, error) { - inst, err := vk.CreateInstance("VK_KHR_surface", "VK_KHR_xlib_surface") - if err != nil { - return nil, err - } - disp := w.display() - window, _, _ := w.window() - surf, err := vk.CreateXlibSurface(inst, unsafe.Pointer(disp), uintptr(window)) - if err != nil { - vk.DestroyInstance(inst) - return nil, err - } - ctx, err := newVulkanContext(inst, surf) - if err != nil { - vk.DestroySurface(inst, surf) - vk.DestroyInstance(inst) - return nil, err - } - c := &x11VkContext{ - win: w, - inst: inst, - surf: surf, - ctx: ctx, - } - return c, nil - } -} - -func (c *x11VkContext) RenderTarget() (gpu.RenderTarget, error) { - return c.ctx.RenderTarget() -} - -func (c *x11VkContext) API() gpu.API { - return c.ctx.api() -} - -func (c *x11VkContext) Release() { - c.ctx.release() - vk.DestroySurface(c.inst, c.surf) - vk.DestroyInstance(c.inst) - *c = x11VkContext{} -} - -func (c *x11VkContext) Present() error { - return c.ctx.present() -} - -func (c *x11VkContext) Lock() error { - return nil -} - -func (c *x11VkContext) Unlock() {} - -func (c *x11VkContext) Refresh() error { - _, w, h := c.win.window() - return c.ctx.refresh(c.surf, w, h) -} diff --git a/gio/app/wayland_text_input.c b/gio/app/wayland_text_input.c deleted file mode 100644 index 65de0bb..0000000 --- a/gio/app/wayland_text_input.c +++ /dev/null @@ -1,100 +0,0 @@ -//go:build ((linux && !android) || freebsd) && !nowayland -// +build linux,!android freebsd -// +build !nowayland - -/* Generated by wayland-scanner 1.19.0 */ - -/* - * Copyright © 2012, 2013 Intel Corporation - * Copyright © 2015, 2016 Jan Arne Petersen - * Copyright © 2017, 2018 Red Hat, Inc. - * Copyright © 2018 Purism SPC - * - * Permission to use, copy, modify, distribute, and sell this - * software and its documentation for any purpose is hereby granted - * without fee, provided that the above copyright notice appear in - * all copies and that both that copyright notice and this permission - * notice appear in supporting documentation, and that the name of - * the copyright holders not be used in advertising or publicity - * pertaining to distribution of the software without specific, - * written prior permission. The copyright holders make no - * representations about the suitability of this software for any - * purpose. It is provided "as is" without express or implied - * warranty. - * - * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS - * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND - * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY - * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN - * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, - * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF - * THIS SOFTWARE. - */ - -#include -#include -#include "wayland-util.h" - -#ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ -#endif - -#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) -#define WL_PRIVATE __attribute__ ((visibility("hidden"))) -#else -#define WL_PRIVATE -#endif - -extern const struct wl_interface wl_seat_interface; -extern const struct wl_interface wl_surface_interface; -extern const struct wl_interface zwp_text_input_v3_interface; - -static const struct wl_interface *text_input_unstable_v3_types[] = { - NULL, - NULL, - NULL, - NULL, - &wl_surface_interface, - &wl_surface_interface, - &zwp_text_input_v3_interface, - &wl_seat_interface, -}; - -static const struct wl_message zwp_text_input_v3_requests[] = { - { "destroy", "", text_input_unstable_v3_types + 0 }, - { "enable", "", text_input_unstable_v3_types + 0 }, - { "disable", "", text_input_unstable_v3_types + 0 }, - { "set_surrounding_text", "sii", text_input_unstable_v3_types + 0 }, - { "set_text_change_cause", "u", text_input_unstable_v3_types + 0 }, - { "set_content_type", "uu", text_input_unstable_v3_types + 0 }, - { "set_cursor_rectangle", "iiii", text_input_unstable_v3_types + 0 }, - { "commit", "", text_input_unstable_v3_types + 0 }, -}; - -static const struct wl_message zwp_text_input_v3_events[] = { - { "enter", "o", text_input_unstable_v3_types + 4 }, - { "leave", "o", text_input_unstable_v3_types + 5 }, - { "preedit_string", "?sii", text_input_unstable_v3_types + 0 }, - { "commit_string", "?s", text_input_unstable_v3_types + 0 }, - { "delete_surrounding_text", "uu", text_input_unstable_v3_types + 0 }, - { "done", "u", text_input_unstable_v3_types + 0 }, -}; - -WL_PRIVATE const struct wl_interface zwp_text_input_v3_interface = { - "zwp_text_input_v3", 1, - 8, zwp_text_input_v3_requests, - 6, zwp_text_input_v3_events, -}; - -static const struct wl_message zwp_text_input_manager_v3_requests[] = { - { "destroy", "", text_input_unstable_v3_types + 0 }, - { "get_text_input", "no", text_input_unstable_v3_types + 6 }, -}; - -WL_PRIVATE const struct wl_interface zwp_text_input_manager_v3_interface = { - "zwp_text_input_manager_v3", 1, - 2, zwp_text_input_manager_v3_requests, - 0, NULL, -}; - diff --git a/gio/app/wayland_text_input.h b/gio/app/wayland_text_input.h deleted file mode 100644 index 882da43..0000000 --- a/gio/app/wayland_text_input.h +++ /dev/null @@ -1,836 +0,0 @@ -/* Generated by wayland-scanner 1.19.0 */ - -#ifndef TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H -#define TEXT_INPUT_UNSTABLE_V3_CLIENT_PROTOCOL_H - -#include -#include -#include "wayland-client.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @page page_text_input_unstable_v3 The text_input_unstable_v3 protocol - * Protocol for composing text - * - * @section page_desc_text_input_unstable_v3 Description - * - * This protocol allows compositors to act as input methods and to send text - * to applications. A text input object is used to manage state of what are - * typically text entry fields in the application. - * - * This document adheres to the RFC 2119 when using words like "must", - * "should", "may", etc. - * - * Warning! The protocol described in this file is experimental and - * backward incompatible changes may be made. Backward compatible changes - * may be added together with the corresponding interface version bump. - * Backward incompatible changes are done by bumping the version number in - * the protocol and interface names and resetting the interface version. - * Once the protocol is to be declared stable, the 'z' prefix and the - * version number in the protocol and interface names are removed and the - * interface version number is reset. - * - * @section page_ifaces_text_input_unstable_v3 Interfaces - * - @subpage page_iface_zwp_text_input_v3 - text input - * - @subpage page_iface_zwp_text_input_manager_v3 - text input manager - * @section page_copyright_text_input_unstable_v3 Copyright - *
- *
- * Copyright © 2012, 2013 Intel Corporation
- * Copyright © 2015, 2016 Jan Arne Petersen
- * Copyright © 2017, 2018 Red Hat, Inc.
- * Copyright © 2018       Purism SPC
- *
- * Permission to use, copy, modify, distribute, and sell this
- * software and its documentation for any purpose is hereby granted
- * without fee, provided that the above copyright notice appear in
- * all copies and that both that copyright notice and this permission
- * notice appear in supporting documentation, and that the name of
- * the copyright holders not be used in advertising or publicity
- * pertaining to distribution of the software without specific,
- * written prior permission.  The copyright holders make no
- * representations about the suitability of this software for any
- * purpose.  It is provided "as is" without express or implied
- * warranty.
- *
- * THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
- * SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
- * FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
- * SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
- * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
- * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
- * THIS SOFTWARE.
- * 
- */ -struct wl_seat; -struct wl_surface; -struct zwp_text_input_manager_v3; -struct zwp_text_input_v3; - -#ifndef ZWP_TEXT_INPUT_V3_INTERFACE -#define ZWP_TEXT_INPUT_V3_INTERFACE -/** - * @page page_iface_zwp_text_input_v3 zwp_text_input_v3 - * @section page_iface_zwp_text_input_v3_desc Description - * - * The zwp_text_input_v3 interface represents text input and input methods - * associated with a seat. It provides enter/leave events to follow the - * text input focus for a seat. - * - * Requests are used to enable/disable the text-input object and set - * state information like surrounding and selected text or the content type. - * The information about the entered text is sent to the text-input object - * via the preedit_string and commit_string events. - * - * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices - * must not point to middle bytes inside a code point: they must either - * point to the first byte of a code point or to the end of the buffer. - * Lengths must be measured between two valid indices. - * - * Focus moving throughout surfaces will result in the emission of - * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused - * surface must commit zwp_text_input_v3.enable and - * zwp_text_input_v3.disable requests as the keyboard focus moves across - * editable and non-editable elements of the UI. Those two requests are not - * expected to be paired with each other, the compositor must be able to - * handle consecutive series of the same request. - * - * State is sent by the state requests (set_surrounding_text, - * set_content_type and set_cursor_rectangle) and a commit request. After an - * enter event or disable request all state information is invalidated and - * needs to be resent by the client. - * @section page_iface_zwp_text_input_v3_api API - * See @ref iface_zwp_text_input_v3. - */ -/** - * @defgroup iface_zwp_text_input_v3 The zwp_text_input_v3 interface - * - * The zwp_text_input_v3 interface represents text input and input methods - * associated with a seat. It provides enter/leave events to follow the - * text input focus for a seat. - * - * Requests are used to enable/disable the text-input object and set - * state information like surrounding and selected text or the content type. - * The information about the entered text is sent to the text-input object - * via the preedit_string and commit_string events. - * - * Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices - * must not point to middle bytes inside a code point: they must either - * point to the first byte of a code point or to the end of the buffer. - * Lengths must be measured between two valid indices. - * - * Focus moving throughout surfaces will result in the emission of - * zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused - * surface must commit zwp_text_input_v3.enable and - * zwp_text_input_v3.disable requests as the keyboard focus moves across - * editable and non-editable elements of the UI. Those two requests are not - * expected to be paired with each other, the compositor must be able to - * handle consecutive series of the same request. - * - * State is sent by the state requests (set_surrounding_text, - * set_content_type and set_cursor_rectangle) and a commit request. After an - * enter event or disable request all state information is invalidated and - * needs to be resent by the client. - */ -extern const struct wl_interface zwp_text_input_v3_interface; -#endif -#ifndef ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE -#define ZWP_TEXT_INPUT_MANAGER_V3_INTERFACE -/** - * @page page_iface_zwp_text_input_manager_v3 zwp_text_input_manager_v3 - * @section page_iface_zwp_text_input_manager_v3_desc Description - * - * A factory for text-input objects. This object is a global singleton. - * @section page_iface_zwp_text_input_manager_v3_api API - * See @ref iface_zwp_text_input_manager_v3. - */ -/** - * @defgroup iface_zwp_text_input_manager_v3 The zwp_text_input_manager_v3 interface - * - * A factory for text-input objects. This object is a global singleton. - */ -extern const struct wl_interface zwp_text_input_manager_v3_interface; -#endif - -#ifndef ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM -#define ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * text change reason - * - * Reason for the change of surrounding text or cursor posision. - */ -enum zwp_text_input_v3_change_cause { - /** - * input method caused the change - */ - ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD = 0, - /** - * something else than the input method caused the change - */ - ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_OTHER = 1, -}; -#endif /* ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_ENUM */ - -#ifndef ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM -#define ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * content hint - * - * Content hint is a bitmask to allow to modify the behavior of the text - * input. - */ -enum zwp_text_input_v3_content_hint { - /** - * no special behavior - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE = 0x0, - /** - * suggest word completions - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_COMPLETION = 0x1, - /** - * suggest word corrections - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_SPELLCHECK = 0x2, - /** - * switch to uppercase letters at the start of a sentence - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_AUTO_CAPITALIZATION = 0x4, - /** - * prefer lowercase letters - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_LOWERCASE = 0x8, - /** - * prefer uppercase letters - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_UPPERCASE = 0x10, - /** - * prefer casing for titles and headings (can be language dependent) - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_TITLECASE = 0x20, - /** - * characters should be hidden - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_HIDDEN_TEXT = 0x40, - /** - * typed text should not be stored - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_SENSITIVE_DATA = 0x80, - /** - * just Latin characters should be entered - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_LATIN = 0x100, - /** - * the text input is multiline - */ - ZWP_TEXT_INPUT_V3_CONTENT_HINT_MULTILINE = 0x200, -}; -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_HINT_ENUM */ - -#ifndef ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM -#define ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM -/** - * @ingroup iface_zwp_text_input_v3 - * content purpose - * - * The content purpose allows to specify the primary purpose of a text - * input. - * - * This allows an input method to show special purpose input panels with - * extra characters or to disallow some characters. - */ -enum zwp_text_input_v3_content_purpose { - /** - * default input, allowing all characters - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL = 0, - /** - * allow only alphabetic characters - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ALPHA = 1, - /** - * allow only digits - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DIGITS = 2, - /** - * input a number (including decimal separator and sign) - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NUMBER = 3, - /** - * input a phone number - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PHONE = 4, - /** - * input an URL - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_URL = 5, - /** - * input an email address - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_EMAIL = 6, - /** - * input a name of a person - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NAME = 7, - /** - * input a password (combine with sensitive_data hint) - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PASSWORD = 8, - /** - * input is a numeric password (combine with sensitive_data hint) - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_PIN = 9, - /** - * input a date - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATE = 10, - /** - * input a time - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TIME = 11, - /** - * input a date and time - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_DATETIME = 12, - /** - * input for a terminal - */ - ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_TERMINAL = 13, -}; -#endif /* ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_ENUM */ - -/** - * @ingroup iface_zwp_text_input_v3 - * @struct zwp_text_input_v3_listener - */ -struct zwp_text_input_v3_listener { - /** - * enter event - * - * Notification that this seat's text-input focus is on a certain - * surface. - * - * If client has created multiple text input objects, compositor - * must send this event to all of them. - * - * When the seat has the keyboard capability the text-input focus - * follows the keyboard focus. This event sets the current surface - * for the text-input object. - */ - void (*enter)(void *data, - struct zwp_text_input_v3 *zwp_text_input_v3, - struct wl_surface *surface); - /** - * leave event - * - * Notification that this seat's text-input focus is no longer on - * a certain surface. The client should reset any preedit string - * previously set. - * - * The leave notification clears the current surface. It is sent - * before the enter notification for the new focus. After leave - * event, compositor must ignore requests from any text input - * instances until next enter event. - * - * When the seat has the keyboard capability the text-input focus - * follows the keyboard focus. - */ - void (*leave)(void *data, - struct zwp_text_input_v3 *zwp_text_input_v3, - struct wl_surface *surface); - /** - * pre-edit - * - * Notify when a new composing text (pre-edit) should be set at - * the current cursor position. Any previously set composing text - * must be removed. Any previously existing selected text must be - * removed. - * - * The argument text contains the pre-edit string buffer. - * - * The parameters cursor_begin and cursor_end are counted in bytes - * relative to the beginning of the submitted text buffer. Cursor - * should be hidden when both are equal to -1. - * - * They could be represented by the client as a line if both values - * are the same, or as a text highlight otherwise. - * - * Values set with this event are double-buffered. They must be - * applied and reset to initial on the next zwp_text_input_v3.done - * event. - * - * The initial value of text is an empty string, and cursor_begin, - * cursor_end and cursor_hidden are all 0. - */ - void (*preedit_string)(void *data, - struct zwp_text_input_v3 *zwp_text_input_v3, - const char *text, - int32_t cursor_begin, - int32_t cursor_end); - /** - * text commit - * - * Notify when text should be inserted into the editor widget. - * The text to commit could be either just a single character after - * a key press or the result of some composing (pre-edit). - * - * Values set with this event are double-buffered. They must be - * applied and reset to initial on the next zwp_text_input_v3.done - * event. - * - * The initial value of text is an empty string. - */ - void (*commit_string)(void *data, - struct zwp_text_input_v3 *zwp_text_input_v3, - const char *text); - /** - * delete surrounding text - * - * Notify when the text around the current cursor position should - * be deleted. - * - * Before_length and after_length are the number of bytes before - * and after the current cursor index (excluding the selection) to - * delete. - * - * If a preedit text is present, in effect before_length is counted - * from the beginning of it, and after_length from its end (see - * done event sequence). - * - * Values set with this event are double-buffered. They must be - * applied and reset to initial on the next zwp_text_input_v3.done - * event. - * - * The initial values of both before_length and after_length are 0. - * @param before_length length of text before current cursor position - * @param after_length length of text after current cursor position - */ - void (*delete_surrounding_text)(void *data, - struct zwp_text_input_v3 *zwp_text_input_v3, - uint32_t before_length, - uint32_t after_length); - /** - * apply changes - * - * Instruct the application to apply changes to state requested - * by the preedit_string, commit_string and delete_surrounding_text - * events. The state relating to these events is double-buffered, - * and each one modifies the pending state. This event replaces the - * current state with the pending state. - * - * The application must proceed by evaluating the changes in the - * following order: - * - * 1. Replace existing preedit string with the cursor. 2. Delete - * requested surrounding text. 3. Insert commit string with the - * cursor at its end. 4. Calculate surrounding text to send. 5. - * Insert new preedit text in cursor position. 6. Place cursor - * inside preedit text. - * - * The serial number reflects the last state of the - * zwp_text_input_v3 object known to the compositor. The value of - * the serial argument must be equal to the number of commit - * requests already issued on that object. When the client receives - * a done event with a serial different than the number of past - * commit requests, it must proceed as normal, except it should not - * change the current state of the zwp_text_input_v3 object. - */ - void (*done)(void *data, - struct zwp_text_input_v3 *zwp_text_input_v3, - uint32_t serial); -}; - -/** - * @ingroup iface_zwp_text_input_v3 - */ -static inline int -zwp_text_input_v3_add_listener(struct zwp_text_input_v3 *zwp_text_input_v3, - const struct zwp_text_input_v3_listener *listener, void *data) -{ - return wl_proxy_add_listener((struct wl_proxy *) zwp_text_input_v3, - (void (**)(void)) listener, data); -} - -#define ZWP_TEXT_INPUT_V3_DESTROY 0 -#define ZWP_TEXT_INPUT_V3_ENABLE 1 -#define ZWP_TEXT_INPUT_V3_DISABLE 2 -#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT 3 -#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE 4 -#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE 5 -#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE 6 -#define ZWP_TEXT_INPUT_V3_COMMIT 7 - -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_ENTER_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_LEAVE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_PREEDIT_STRING_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_COMMIT_STRING_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_DELETE_SURROUNDING_TEXT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_DONE_SINCE_VERSION 1 - -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_ENABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_DISABLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_v3 - */ -#define ZWP_TEXT_INPUT_V3_COMMIT_SINCE_VERSION 1 - -/** @ingroup iface_zwp_text_input_v3 */ -static inline void -zwp_text_input_v3_set_user_data(struct zwp_text_input_v3 *zwp_text_input_v3, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_v3, user_data); -} - -/** @ingroup iface_zwp_text_input_v3 */ -static inline void * -zwp_text_input_v3_get_user_data(struct zwp_text_input_v3 *zwp_text_input_v3) -{ - return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_v3); -} - -static inline uint32_t -zwp_text_input_v3_get_version(struct zwp_text_input_v3 *zwp_text_input_v3) -{ - return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_v3); -} - -/** - * @ingroup iface_zwp_text_input_v3 - * - * Destroy the wp_text_input object. Also disables all surfaces enabled - * through this wp_text_input object. - */ -static inline void -zwp_text_input_v3_destroy(struct zwp_text_input_v3 *zwp_text_input_v3) -{ - wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, - ZWP_TEXT_INPUT_V3_DESTROY); - - wl_proxy_destroy((struct wl_proxy *) zwp_text_input_v3); -} - -/** - * @ingroup iface_zwp_text_input_v3 - * - * Requests text input on the surface previously obtained from the enter - * event. - * - * This request must be issued every time the active text input changes - * to a new one, including within the current surface. Use - * zwp_text_input_v3.disable when there is no longer any input focus on - * the current surface. - * - * Clients must not enable more than one text input on the single seat - * and should disable the current text input before enabling the new one. - * At most one instance of text input may be in enabled state per instance, - * Requests to enable the another text input when some text input is active - * must be ignored by compositor. - * - * This request resets all state associated with previous enable, disable, - * set_surrounding_text, set_text_change_cause, set_content_type, and - * set_cursor_rectangle requests, as well as the state associated with - * preedit_string, commit_string, and delete_surrounding_text events. - * - * The set_surrounding_text, set_content_type and set_cursor_rectangle - * requests must follow if the text input supports the necessary - * functionality. - * - * State set with this request is double-buffered. It will get applied on - * the next zwp_text_input_v3.commit request, and stay valid until the - * next committed enable or disable request. - * - * The changes must be applied by the compositor after issuing a - * zwp_text_input_v3.commit request. - */ -static inline void -zwp_text_input_v3_enable(struct zwp_text_input_v3 *zwp_text_input_v3) -{ - wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, - ZWP_TEXT_INPUT_V3_ENABLE); -} - -/** - * @ingroup iface_zwp_text_input_v3 - * - * Explicitly disable text input on the current surface (typically when - * there is no focus on any text entry inside the surface). - * - * State set with this request is double-buffered. It will get applied on - * the next zwp_text_input_v3.commit request. - */ -static inline void -zwp_text_input_v3_disable(struct zwp_text_input_v3 *zwp_text_input_v3) -{ - wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, - ZWP_TEXT_INPUT_V3_DISABLE); -} - -/** - * @ingroup iface_zwp_text_input_v3 - * - * Sets the surrounding plain text around the input, excluding the preedit - * text. - * - * The client should notify the compositor of any changes in any of the - * values carried with this request, including changes caused by handling - * incoming text-input events as well as changes caused by other - * mechanisms like keyboard typing. - * - * If the client is unaware of the text around the cursor, it should not - * issue this request, to signify lack of support to the compositor. - * - * Text is UTF-8 encoded, and should include the cursor position, the - * complete selection and additional characters before and after them. - * There is a maximum length of wayland messages, so text can not be - * longer than 4000 bytes. - * - * Cursor is the byte offset of the cursor within text buffer. - * - * Anchor is the byte offset of the selection anchor within text buffer. - * If there is no selected text, anchor is the same as cursor. - * - * If any preedit text is present, it is replaced with a cursor for the - * purpose of this event. - * - * Values set with this request are double-buffered. They will get applied - * on the next zwp_text_input_v3.commit request, and stay valid until the - * next committed enable or disable request. - * - * The initial state for affected fields is empty, meaning that the text - * input does not support sending surrounding text. If the empty values - * get applied, subsequent attempts to change them may have no effect. - */ -static inline void -zwp_text_input_v3_set_surrounding_text(struct zwp_text_input_v3 *zwp_text_input_v3, const char *text, int32_t cursor, int32_t anchor) -{ - wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, - ZWP_TEXT_INPUT_V3_SET_SURROUNDING_TEXT, text, cursor, anchor); -} - -/** - * @ingroup iface_zwp_text_input_v3 - * - * Tells the compositor why the text surrounding the cursor changed. - * - * Whenever the client detects an external change in text, cursor, or - * anchor posision, it must issue this request to the compositor. This - * request is intended to give the input method a chance to update the - * preedit text in an appropriate way, e.g. by removing it when the user - * starts typing with a keyboard. - * - * cause describes the source of the change. - * - * The value set with this request is double-buffered. It must be applied - * and reset to initial at the next zwp_text_input_v3.commit request. - * - * The initial value of cause is input_method. - */ -static inline void -zwp_text_input_v3_set_text_change_cause(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t cause) -{ - wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, - ZWP_TEXT_INPUT_V3_SET_TEXT_CHANGE_CAUSE, cause); -} - -/** - * @ingroup iface_zwp_text_input_v3 - * - * Sets the content purpose and content hint. While the purpose is the - * basic purpose of an input field, the hint flags allow to modify some of - * the behavior. - * - * Values set with this request are double-buffered. They will get applied - * on the next zwp_text_input_v3.commit request. - * Subsequent attempts to update them may have no effect. The values - * remain valid until the next committed enable or disable request. - * - * The initial value for hint is none, and the initial value for purpose - * is normal. - */ -static inline void -zwp_text_input_v3_set_content_type(struct zwp_text_input_v3 *zwp_text_input_v3, uint32_t hint, uint32_t purpose) -{ - wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, - ZWP_TEXT_INPUT_V3_SET_CONTENT_TYPE, hint, purpose); -} - -/** - * @ingroup iface_zwp_text_input_v3 - * - * Marks an area around the cursor as a x, y, width, height rectangle in - * surface local coordinates. - * - * Allows the compositor to put a window with word suggestions near the - * cursor, without obstructing the text being input. - * - * If the client is unaware of the position of edited text, it should not - * issue this request, to signify lack of support to the compositor. - * - * Values set with this request are double-buffered. They will get applied - * on the next zwp_text_input_v3.commit request, and stay valid until the - * next committed enable or disable request. - * - * The initial values describing a cursor rectangle are empty. That means - * the text input does not support describing the cursor area. If the - * empty values get applied, subsequent attempts to change them may have - * no effect. - */ -static inline void -zwp_text_input_v3_set_cursor_rectangle(struct zwp_text_input_v3 *zwp_text_input_v3, int32_t x, int32_t y, int32_t width, int32_t height) -{ - wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, - ZWP_TEXT_INPUT_V3_SET_CURSOR_RECTANGLE, x, y, width, height); -} - -/** - * @ingroup iface_zwp_text_input_v3 - * - * Atomically applies state changes recently sent to the compositor. - * - * The commit request establishes and updates the state of the client, and - * must be issued after any changes to apply them. - * - * Text input state (enabled status, content purpose, content hint, - * surrounding text and change cause, cursor rectangle) is conceptually - * double-buffered within the context of a text input, i.e. between a - * committed enable request and the following committed enable or disable - * request. - * - * Protocol requests modify the pending state, as opposed to the current - * state in use by the input method. A commit request atomically applies - * all pending state, replacing the current state. After commit, the new - * pending state is as documented for each related request. - * - * Requests are applied in the order of arrival. - * - * Neither current nor pending state are modified unless noted otherwise. - * - * The compositor must count the number of commit requests coming from - * each zwp_text_input_v3 object and use the count as the serial in done - * events. - */ -static inline void -zwp_text_input_v3_commit(struct zwp_text_input_v3 *zwp_text_input_v3) -{ - wl_proxy_marshal((struct wl_proxy *) zwp_text_input_v3, - ZWP_TEXT_INPUT_V3_COMMIT); -} - -#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY 0 -#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT 1 - - -/** - * @ingroup iface_zwp_text_input_manager_v3 - */ -#define ZWP_TEXT_INPUT_MANAGER_V3_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zwp_text_input_manager_v3 - */ -#define ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT_SINCE_VERSION 1 - -/** @ingroup iface_zwp_text_input_manager_v3 */ -static inline void -zwp_text_input_manager_v3_set_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) zwp_text_input_manager_v3, user_data); -} - -/** @ingroup iface_zwp_text_input_manager_v3 */ -static inline void * -zwp_text_input_manager_v3_get_user_data(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) -{ - return wl_proxy_get_user_data((struct wl_proxy *) zwp_text_input_manager_v3); -} - -static inline uint32_t -zwp_text_input_manager_v3_get_version(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) -{ - return wl_proxy_get_version((struct wl_proxy *) zwp_text_input_manager_v3); -} - -/** - * @ingroup iface_zwp_text_input_manager_v3 - * - * Destroy the wp_text_input_manager object. - */ -static inline void -zwp_text_input_manager_v3_destroy(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3) -{ - wl_proxy_marshal((struct wl_proxy *) zwp_text_input_manager_v3, - ZWP_TEXT_INPUT_MANAGER_V3_DESTROY); - - wl_proxy_destroy((struct wl_proxy *) zwp_text_input_manager_v3); -} - -/** - * @ingroup iface_zwp_text_input_manager_v3 - * - * Creates a new text-input object for a given seat. - */ -static inline struct zwp_text_input_v3 * -zwp_text_input_manager_v3_get_text_input(struct zwp_text_input_manager_v3 *zwp_text_input_manager_v3, struct wl_seat *seat) -{ - struct wl_proxy *id; - - id = wl_proxy_marshal_constructor((struct wl_proxy *) zwp_text_input_manager_v3, - ZWP_TEXT_INPUT_MANAGER_V3_GET_TEXT_INPUT, &zwp_text_input_v3_interface, NULL, seat); - - return (struct zwp_text_input_v3 *) id; -} - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/gio/app/wayland_xdg_decoration.c b/gio/app/wayland_xdg_decoration.c deleted file mode 100644 index ee94c60..0000000 --- a/gio/app/wayland_xdg_decoration.c +++ /dev/null @@ -1,79 +0,0 @@ -//go:build ((linux && !android) || freebsd) && !nowayland -// +build linux,!android freebsd -// +build !nowayland - -/* Generated by wayland-scanner 1.19.0 */ - -/* - * Copyright © 2018 Simon Ser - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice (including the next - * paragraph) shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -#include -#include -#include "wayland-util.h" - -#ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ -#endif - -#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) -#define WL_PRIVATE __attribute__ ((visibility("hidden"))) -#else -#define WL_PRIVATE -#endif - -extern const struct wl_interface xdg_toplevel_interface; -extern const struct wl_interface zxdg_toplevel_decoration_v1_interface; - -static const struct wl_interface *xdg_decoration_unstable_v1_types[] = { - NULL, - &zxdg_toplevel_decoration_v1_interface, - &xdg_toplevel_interface, -}; - -static const struct wl_message zxdg_decoration_manager_v1_requests[] = { - { "destroy", "", xdg_decoration_unstable_v1_types + 0 }, - { "get_toplevel_decoration", "no", xdg_decoration_unstable_v1_types + 1 }, -}; - -WL_PRIVATE const struct wl_interface zxdg_decoration_manager_v1_interface = { - "zxdg_decoration_manager_v1", 1, - 2, zxdg_decoration_manager_v1_requests, - 0, NULL, -}; - -static const struct wl_message zxdg_toplevel_decoration_v1_requests[] = { - { "destroy", "", xdg_decoration_unstable_v1_types + 0 }, - { "set_mode", "u", xdg_decoration_unstable_v1_types + 0 }, - { "unset_mode", "", xdg_decoration_unstable_v1_types + 0 }, -}; - -static const struct wl_message zxdg_toplevel_decoration_v1_events[] = { - { "configure", "u", xdg_decoration_unstable_v1_types + 0 }, -}; - -WL_PRIVATE const struct wl_interface zxdg_toplevel_decoration_v1_interface = { - "zxdg_toplevel_decoration_v1", 1, - 3, zxdg_toplevel_decoration_v1_requests, - 1, zxdg_toplevel_decoration_v1_events, -}; - diff --git a/gio/app/wayland_xdg_decoration.h b/gio/app/wayland_xdg_decoration.h deleted file mode 100644 index 004d342..0000000 --- a/gio/app/wayland_xdg_decoration.h +++ /dev/null @@ -1,382 +0,0 @@ -/* Generated by wayland-scanner 1.19.0 */ - -#ifndef XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H -#define XDG_DECORATION_UNSTABLE_V1_CLIENT_PROTOCOL_H - -#include -#include -#include "wayland-client.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @page page_xdg_decoration_unstable_v1 The xdg_decoration_unstable_v1 protocol - * @section page_ifaces_xdg_decoration_unstable_v1 Interfaces - * - @subpage page_iface_zxdg_decoration_manager_v1 - window decoration manager - * - @subpage page_iface_zxdg_toplevel_decoration_v1 - decoration object for a toplevel surface - * @section page_copyright_xdg_decoration_unstable_v1 Copyright - *
- *
- * Copyright © 2018 Simon Ser
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice (including the next
- * paragraph) shall be included in all copies or substantial portions of the
- * Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- * 
- */ -struct xdg_toplevel; -struct zxdg_decoration_manager_v1; -struct zxdg_toplevel_decoration_v1; - -#ifndef ZXDG_DECORATION_MANAGER_V1_INTERFACE -#define ZXDG_DECORATION_MANAGER_V1_INTERFACE -/** - * @page page_iface_zxdg_decoration_manager_v1 zxdg_decoration_manager_v1 - * @section page_iface_zxdg_decoration_manager_v1_desc Description - * - * This interface allows a compositor to announce support for server-side - * decorations. - * - * A window decoration is a set of window controls as deemed appropriate by - * the party managing them, such as user interface components used to move, - * resize and change a window's state. - * - * A client can use this protocol to request being decorated by a supporting - * compositor. - * - * If compositor and client do not negotiate the use of a server-side - * decoration using this protocol, clients continue to self-decorate as they - * see fit. - * - * Warning! The protocol described in this file is experimental and - * backward incompatible changes may be made. Backward compatible changes - * may be added together with the corresponding interface version bump. - * Backward incompatible changes are done by bumping the version number in - * the protocol and interface names and resetting the interface version. - * Once the protocol is to be declared stable, the 'z' prefix and the - * version number in the protocol and interface names are removed and the - * interface version number is reset. - * @section page_iface_zxdg_decoration_manager_v1_api API - * See @ref iface_zxdg_decoration_manager_v1. - */ -/** - * @defgroup iface_zxdg_decoration_manager_v1 The zxdg_decoration_manager_v1 interface - * - * This interface allows a compositor to announce support for server-side - * decorations. - * - * A window decoration is a set of window controls as deemed appropriate by - * the party managing them, such as user interface components used to move, - * resize and change a window's state. - * - * A client can use this protocol to request being decorated by a supporting - * compositor. - * - * If compositor and client do not negotiate the use of a server-side - * decoration using this protocol, clients continue to self-decorate as they - * see fit. - * - * Warning! The protocol described in this file is experimental and - * backward incompatible changes may be made. Backward compatible changes - * may be added together with the corresponding interface version bump. - * Backward incompatible changes are done by bumping the version number in - * the protocol and interface names and resetting the interface version. - * Once the protocol is to be declared stable, the 'z' prefix and the - * version number in the protocol and interface names are removed and the - * interface version number is reset. - */ -extern const struct wl_interface zxdg_decoration_manager_v1_interface; -#endif -#ifndef ZXDG_TOPLEVEL_DECORATION_V1_INTERFACE -#define ZXDG_TOPLEVEL_DECORATION_V1_INTERFACE -/** - * @page page_iface_zxdg_toplevel_decoration_v1 zxdg_toplevel_decoration_v1 - * @section page_iface_zxdg_toplevel_decoration_v1_desc Description - * - * The decoration object allows the compositor to toggle server-side window - * decorations for a toplevel surface. The client can request to switch to - * another mode. - * - * The xdg_toplevel_decoration object must be destroyed before its - * xdg_toplevel. - * @section page_iface_zxdg_toplevel_decoration_v1_api API - * See @ref iface_zxdg_toplevel_decoration_v1. - */ -/** - * @defgroup iface_zxdg_toplevel_decoration_v1 The zxdg_toplevel_decoration_v1 interface - * - * The decoration object allows the compositor to toggle server-side window - * decorations for a toplevel surface. The client can request to switch to - * another mode. - * - * The xdg_toplevel_decoration object must be destroyed before its - * xdg_toplevel. - */ -extern const struct wl_interface zxdg_toplevel_decoration_v1_interface; -#endif - -#define ZXDG_DECORATION_MANAGER_V1_DESTROY 0 -#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION 1 - - -/** - * @ingroup iface_zxdg_decoration_manager_v1 - */ -#define ZXDG_DECORATION_MANAGER_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zxdg_decoration_manager_v1 - */ -#define ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION_SINCE_VERSION 1 - -/** @ingroup iface_zxdg_decoration_manager_v1 */ -static inline void -zxdg_decoration_manager_v1_set_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) zxdg_decoration_manager_v1, user_data); -} - -/** @ingroup iface_zxdg_decoration_manager_v1 */ -static inline void * -zxdg_decoration_manager_v1_get_user_data(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) -{ - return wl_proxy_get_user_data((struct wl_proxy *) zxdg_decoration_manager_v1); -} - -static inline uint32_t -zxdg_decoration_manager_v1_get_version(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) -{ - return wl_proxy_get_version((struct wl_proxy *) zxdg_decoration_manager_v1); -} - -/** - * @ingroup iface_zxdg_decoration_manager_v1 - * - * Destroy the decoration manager. This doesn't destroy objects created - * with the manager. - */ -static inline void -zxdg_decoration_manager_v1_destroy(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1) -{ - wl_proxy_marshal((struct wl_proxy *) zxdg_decoration_manager_v1, - ZXDG_DECORATION_MANAGER_V1_DESTROY); - - wl_proxy_destroy((struct wl_proxy *) zxdg_decoration_manager_v1); -} - -/** - * @ingroup iface_zxdg_decoration_manager_v1 - * - * Create a new decoration object associated with the given toplevel. - * - * Creating an xdg_toplevel_decoration from an xdg_toplevel which has a - * buffer attached or committed is a client error, and any attempts by a - * client to attach or manipulate a buffer prior to the first - * xdg_toplevel_decoration.configure event must also be treated as - * errors. - */ -static inline struct zxdg_toplevel_decoration_v1 * -zxdg_decoration_manager_v1_get_toplevel_decoration(struct zxdg_decoration_manager_v1 *zxdg_decoration_manager_v1, struct xdg_toplevel *toplevel) -{ - struct wl_proxy *id; - - id = wl_proxy_marshal_constructor((struct wl_proxy *) zxdg_decoration_manager_v1, - ZXDG_DECORATION_MANAGER_V1_GET_TOPLEVEL_DECORATION, &zxdg_toplevel_decoration_v1_interface, NULL, toplevel); - - return (struct zxdg_toplevel_decoration_v1 *) id; -} - -#ifndef ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM -#define ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM -enum zxdg_toplevel_decoration_v1_error { - /** - * xdg_toplevel has a buffer attached before configure - */ - ZXDG_TOPLEVEL_DECORATION_V1_ERROR_UNCONFIGURED_BUFFER = 0, - /** - * xdg_toplevel already has a decoration object - */ - ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ALREADY_CONSTRUCTED = 1, - /** - * xdg_toplevel destroyed before the decoration object - */ - ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ORPHANED = 2, -}; -#endif /* ZXDG_TOPLEVEL_DECORATION_V1_ERROR_ENUM */ - -#ifndef ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM -#define ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - * window decoration modes - * - * These values describe window decoration modes. - */ -enum zxdg_toplevel_decoration_v1_mode { - /** - * no server-side window decoration - */ - ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE = 1, - /** - * server-side window decoration - */ - ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE = 2, -}; -#endif /* ZXDG_TOPLEVEL_DECORATION_V1_MODE_ENUM */ - -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - * @struct zxdg_toplevel_decoration_v1_listener - */ -struct zxdg_toplevel_decoration_v1_listener { - /** - * suggest a surface change - * - * The configure event asks the client to change its decoration - * mode. The configured state should not be applied immediately. - * Clients must send an ack_configure in response to this event. - * See xdg_surface.configure and xdg_surface.ack_configure for - * details. - * - * A configure event can be sent at any time. The specified mode - * must be obeyed by the client. - * @param mode the decoration mode - */ - void (*configure)(void *data, - struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, - uint32_t mode); -}; - -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - */ -static inline int -zxdg_toplevel_decoration_v1_add_listener(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, - const struct zxdg_toplevel_decoration_v1_listener *listener, void *data) -{ - return wl_proxy_add_listener((struct wl_proxy *) zxdg_toplevel_decoration_v1, - (void (**)(void)) listener, data); -} - -#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY 0 -#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE 1 -#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE 2 - -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - */ -#define ZXDG_TOPLEVEL_DECORATION_V1_CONFIGURE_SINCE_VERSION 1 - -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - */ -#define ZXDG_TOPLEVEL_DECORATION_V1_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - */ -#define ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE_SINCE_VERSION 1 -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - */ -#define ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE_SINCE_VERSION 1 - -/** @ingroup iface_zxdg_toplevel_decoration_v1 */ -static inline void -zxdg_toplevel_decoration_v1_set_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1, user_data); -} - -/** @ingroup iface_zxdg_toplevel_decoration_v1 */ -static inline void * -zxdg_toplevel_decoration_v1_get_user_data(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) -{ - return wl_proxy_get_user_data((struct wl_proxy *) zxdg_toplevel_decoration_v1); -} - -static inline uint32_t -zxdg_toplevel_decoration_v1_get_version(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) -{ - return wl_proxy_get_version((struct wl_proxy *) zxdg_toplevel_decoration_v1); -} - -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - * - * Switch back to a mode without any server-side decorations at the next - * commit. - */ -static inline void -zxdg_toplevel_decoration_v1_destroy(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) -{ - wl_proxy_marshal((struct wl_proxy *) zxdg_toplevel_decoration_v1, - ZXDG_TOPLEVEL_DECORATION_V1_DESTROY); - - wl_proxy_destroy((struct wl_proxy *) zxdg_toplevel_decoration_v1); -} - -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - * - * Set the toplevel surface decoration mode. This informs the compositor - * that the client prefers the provided decoration mode. - * - * After requesting a decoration mode, the compositor will respond by - * emitting an xdg_surface.configure event. The client should then update - * its content, drawing it without decorations if the received mode is - * server-side decorations. The client must also acknowledge the configure - * when committing the new content (see xdg_surface.ack_configure). - * - * The compositor can decide not to use the client's mode and enforce a - * different mode instead. - * - * Clients whose decoration mode depend on the xdg_toplevel state may send - * a set_mode request in response to an xdg_surface.configure event and wait - * for the next xdg_surface.configure event to prevent unwanted state. - * Such clients are responsible for preventing configure loops and must - * make sure not to send multiple successive set_mode requests with the - * same decoration mode. - */ -static inline void -zxdg_toplevel_decoration_v1_set_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1, uint32_t mode) -{ - wl_proxy_marshal((struct wl_proxy *) zxdg_toplevel_decoration_v1, - ZXDG_TOPLEVEL_DECORATION_V1_SET_MODE, mode); -} - -/** - * @ingroup iface_zxdg_toplevel_decoration_v1 - * - * Unset the toplevel surface decoration mode. This informs the compositor - * that the client doesn't prefer a particular decoration mode. - * - * This request has the same semantics as set_mode. - */ -static inline void -zxdg_toplevel_decoration_v1_unset_mode(struct zxdg_toplevel_decoration_v1 *zxdg_toplevel_decoration_v1) -{ - wl_proxy_marshal((struct wl_proxy *) zxdg_toplevel_decoration_v1, - ZXDG_TOPLEVEL_DECORATION_V1_UNSET_MODE); -} - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/gio/app/wayland_xdg_shell.c b/gio/app/wayland_xdg_shell.c deleted file mode 100644 index 54fe5cf..0000000 --- a/gio/app/wayland_xdg_shell.c +++ /dev/null @@ -1,185 +0,0 @@ -//go:build ((linux && !android) || freebsd) && !nowayland -// +build linux,!android freebsd -// +build !nowayland - -/* Generated by wayland-scanner 1.19.0 */ - -/* - * Copyright © 2008-2013 Kristian Høgsberg - * Copyright © 2013 Rafael Antognolli - * Copyright © 2013 Jasper St. Pierre - * Copyright © 2010-2013 Intel Corporation - * Copyright © 2015-2017 Samsung Electronics Co., Ltd - * Copyright © 2015-2017 Red Hat Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice (including the next - * paragraph) shall be included in all copies or substantial portions of the - * Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - */ - -#include -#include -#include "wayland-util.h" - -#ifndef __has_attribute -# define __has_attribute(x) 0 /* Compatibility with non-clang compilers. */ -#endif - -#if (__has_attribute(visibility) || defined(__GNUC__) && __GNUC__ >= 4) -#define WL_PRIVATE __attribute__ ((visibility("hidden"))) -#else -#define WL_PRIVATE -#endif - -extern const struct wl_interface wl_output_interface; -extern const struct wl_interface wl_seat_interface; -extern const struct wl_interface wl_surface_interface; -extern const struct wl_interface xdg_popup_interface; -extern const struct wl_interface xdg_positioner_interface; -extern const struct wl_interface xdg_surface_interface; -extern const struct wl_interface xdg_toplevel_interface; - -static const struct wl_interface *xdg_shell_types[] = { - NULL, - NULL, - NULL, - NULL, - &xdg_positioner_interface, - &xdg_surface_interface, - &wl_surface_interface, - &xdg_toplevel_interface, - &xdg_popup_interface, - &xdg_surface_interface, - &xdg_positioner_interface, - &xdg_toplevel_interface, - &wl_seat_interface, - NULL, - NULL, - NULL, - &wl_seat_interface, - NULL, - &wl_seat_interface, - NULL, - NULL, - &wl_output_interface, - &wl_seat_interface, - NULL, - &xdg_positioner_interface, - NULL, -}; - -static const struct wl_message xdg_wm_base_requests[] = { - { "destroy", "", xdg_shell_types + 0 }, - { "create_positioner", "n", xdg_shell_types + 4 }, - { "get_xdg_surface", "no", xdg_shell_types + 5 }, - { "pong", "u", xdg_shell_types + 0 }, -}; - -static const struct wl_message xdg_wm_base_events[] = { - { "ping", "u", xdg_shell_types + 0 }, -}; - -WL_PRIVATE const struct wl_interface xdg_wm_base_interface = { - "xdg_wm_base", 3, - 4, xdg_wm_base_requests, - 1, xdg_wm_base_events, -}; - -static const struct wl_message xdg_positioner_requests[] = { - { "destroy", "", xdg_shell_types + 0 }, - { "set_size", "ii", xdg_shell_types + 0 }, - { "set_anchor_rect", "iiii", xdg_shell_types + 0 }, - { "set_anchor", "u", xdg_shell_types + 0 }, - { "set_gravity", "u", xdg_shell_types + 0 }, - { "set_constraint_adjustment", "u", xdg_shell_types + 0 }, - { "set_offset", "ii", xdg_shell_types + 0 }, - { "set_reactive", "3", xdg_shell_types + 0 }, - { "set_parent_size", "3ii", xdg_shell_types + 0 }, - { "set_parent_configure", "3u", xdg_shell_types + 0 }, -}; - -WL_PRIVATE const struct wl_interface xdg_positioner_interface = { - "xdg_positioner", 3, - 10, xdg_positioner_requests, - 0, NULL, -}; - -static const struct wl_message xdg_surface_requests[] = { - { "destroy", "", xdg_shell_types + 0 }, - { "get_toplevel", "n", xdg_shell_types + 7 }, - { "get_popup", "n?oo", xdg_shell_types + 8 }, - { "set_window_geometry", "iiii", xdg_shell_types + 0 }, - { "ack_configure", "u", xdg_shell_types + 0 }, -}; - -static const struct wl_message xdg_surface_events[] = { - { "configure", "u", xdg_shell_types + 0 }, -}; - -WL_PRIVATE const struct wl_interface xdg_surface_interface = { - "xdg_surface", 3, - 5, xdg_surface_requests, - 1, xdg_surface_events, -}; - -static const struct wl_message xdg_toplevel_requests[] = { - { "destroy", "", xdg_shell_types + 0 }, - { "set_parent", "?o", xdg_shell_types + 11 }, - { "set_title", "s", xdg_shell_types + 0 }, - { "set_app_id", "s", xdg_shell_types + 0 }, - { "show_window_menu", "ouii", xdg_shell_types + 12 }, - { "move", "ou", xdg_shell_types + 16 }, - { "resize", "ouu", xdg_shell_types + 18 }, - { "set_max_size", "ii", xdg_shell_types + 0 }, - { "set_min_size", "ii", xdg_shell_types + 0 }, - { "set_maximized", "", xdg_shell_types + 0 }, - { "unset_maximized", "", xdg_shell_types + 0 }, - { "set_fullscreen", "?o", xdg_shell_types + 21 }, - { "unset_fullscreen", "", xdg_shell_types + 0 }, - { "set_minimized", "", xdg_shell_types + 0 }, -}; - -static const struct wl_message xdg_toplevel_events[] = { - { "configure", "iia", xdg_shell_types + 0 }, - { "close", "", xdg_shell_types + 0 }, -}; - -WL_PRIVATE const struct wl_interface xdg_toplevel_interface = { - "xdg_toplevel", 3, - 14, xdg_toplevel_requests, - 2, xdg_toplevel_events, -}; - -static const struct wl_message xdg_popup_requests[] = { - { "destroy", "", xdg_shell_types + 0 }, - { "grab", "ou", xdg_shell_types + 22 }, - { "reposition", "3ou", xdg_shell_types + 24 }, -}; - -static const struct wl_message xdg_popup_events[] = { - { "configure", "iiii", xdg_shell_types + 0 }, - { "popup_done", "", xdg_shell_types + 0 }, - { "repositioned", "3u", xdg_shell_types + 0 }, -}; - -WL_PRIVATE const struct wl_interface xdg_popup_interface = { - "xdg_popup", 3, - 3, xdg_popup_requests, - 3, xdg_popup_events, -}; - diff --git a/gio/app/wayland_xdg_shell.h b/gio/app/wayland_xdg_shell.h deleted file mode 100644 index 1db8fd9..0000000 --- a/gio/app/wayland_xdg_shell.h +++ /dev/null @@ -1,2003 +0,0 @@ -/* Generated by wayland-scanner 1.19.0 */ - -#ifndef XDG_SHELL_CLIENT_PROTOCOL_H -#define XDG_SHELL_CLIENT_PROTOCOL_H - -#include -#include -#include "wayland-client.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @page page_xdg_shell The xdg_shell protocol - * @section page_ifaces_xdg_shell Interfaces - * - @subpage page_iface_xdg_wm_base - create desktop-style surfaces - * - @subpage page_iface_xdg_positioner - child surface positioner - * - @subpage page_iface_xdg_surface - desktop user interface surface base interface - * - @subpage page_iface_xdg_toplevel - toplevel surface - * - @subpage page_iface_xdg_popup - short-lived, popup surfaces for menus - * @section page_copyright_xdg_shell Copyright - *
- *
- * Copyright © 2008-2013 Kristian Høgsberg
- * Copyright © 2013      Rafael Antognolli
- * Copyright © 2013      Jasper St. Pierre
- * Copyright © 2010-2013 Intel Corporation
- * Copyright © 2015-2017 Samsung Electronics Co., Ltd
- * Copyright © 2015-2017 Red Hat Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a
- * copy of this software and associated documentation files (the "Software"),
- * to deal in the Software without restriction, including without limitation
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
- * and/or sell copies of the Software, and to permit persons to whom the
- * Software is furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice (including the next
- * paragraph) shall be included in all copies or substantial portions of the
- * Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
- * DEALINGS IN THE SOFTWARE.
- * 
- */ -struct wl_output; -struct wl_seat; -struct wl_surface; -struct xdg_popup; -struct xdg_positioner; -struct xdg_surface; -struct xdg_toplevel; -struct xdg_wm_base; - -#ifndef XDG_WM_BASE_INTERFACE -#define XDG_WM_BASE_INTERFACE -/** - * @page page_iface_xdg_wm_base xdg_wm_base - * @section page_iface_xdg_wm_base_desc Description - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - * @section page_iface_xdg_wm_base_api API - * See @ref iface_xdg_wm_base. - */ -/** - * @defgroup iface_xdg_wm_base The xdg_wm_base interface - * - * The xdg_wm_base interface is exposed as a global object enabling clients - * to turn their wl_surfaces into windows in a desktop environment. It - * defines the basic functionality needed for clients and the compositor to - * create windows that can be dragged, resized, maximized, etc, as well as - * creating transient windows such as popup menus. - */ -extern const struct wl_interface xdg_wm_base_interface; -#endif -#ifndef XDG_POSITIONER_INTERFACE -#define XDG_POSITIONER_INTERFACE -/** - * @page page_iface_xdg_positioner xdg_positioner - * @section page_iface_xdg_positioner_desc Description - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an error. - * @section page_iface_xdg_positioner_api API - * See @ref iface_xdg_positioner. - */ -/** - * @defgroup iface_xdg_positioner The xdg_positioner interface - * - * The xdg_positioner provides a collection of rules for the placement of a - * child surface relative to a parent surface. Rules can be defined to ensure - * the child surface remains within the visible area's borders, and to - * specify how the child surface changes its position, such as sliding along - * an axis, or flipping around a rectangle. These positioner-created rules are - * constrained by the requirement that a child surface must intersect with or - * be at least partially adjacent to its parent surface. - * - * See the various requests for details about possible rules. - * - * At the time of the request, the compositor makes a copy of the rules - * specified by the xdg_positioner. Thus, after the request is complete the - * xdg_positioner object can be destroyed or reused; further changes to the - * object will have no effect on previous usages. - * - * For an xdg_positioner object to be considered complete, it must have a - * non-zero size set by set_size, and a non-zero anchor rectangle set by - * set_anchor_rect. Passing an incomplete xdg_positioner object when - * positioning a surface raises an error. - */ -extern const struct wl_interface xdg_positioner_interface; -#endif -#ifndef XDG_SURFACE_INTERFACE -#define XDG_SURFACE_INTERFACE -/** - * @page page_iface_xdg_surface xdg_surface - * @section page_iface_xdg_surface_desc Description - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed. - * @section page_iface_xdg_surface_api API - * See @ref iface_xdg_surface. - */ -/** - * @defgroup iface_xdg_surface The xdg_surface interface - * - * An interface that may be implemented by a wl_surface, for - * implementations that provide a desktop-style user interface. - * - * It provides a base set of functionality required to construct user - * interface elements requiring management by the compositor, such as - * toplevel windows, menus, etc. The types of functionality are split into - * xdg_surface roles. - * - * Creating an xdg_surface does not set the role for a wl_surface. In order - * to map an xdg_surface, the client must create a role-specific object - * using, e.g., get_toplevel, get_popup. The wl_surface for any given - * xdg_surface can have at most one role, and may not be assigned any role - * not based on xdg_surface. - * - * A role must be assigned before any other requests are made to the - * xdg_surface object. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_surface state to take effect. - * - * Creating an xdg_surface from a wl_surface which has a buffer attached or - * committed is a client error, and any attempts by a client to attach or - * manipulate a buffer prior to the first xdg_surface.configure call must - * also be treated as errors. - * - * After creating a role-specific object and setting it up, the client must - * perform an initial commit without any buffer attached. The compositor - * will reply with an xdg_surface.configure event. The client must - * acknowledge it and is then allowed to attach a buffer to map the surface. - * - * Mapping an xdg_surface-based role surface is defined as making it - * possible for the surface to be shown by the compositor. Note that - * a mapped surface is not guaranteed to be visible once it is mapped. - * - * For an xdg_surface to be mapped by the compositor, the following - * conditions must be met: - * (1) the client has assigned an xdg_surface-based role to the surface - * (2) the client has set and committed the xdg_surface state and the - * role-dependent state to the surface - * (3) the client has committed a buffer to the surface - * - * A newly-unmapped surface is considered to have met condition (1) out - * of the 3 required conditions for mapping a surface if its role surface - * has not been destroyed. - */ -extern const struct wl_interface xdg_surface_interface; -#endif -#ifndef XDG_TOPLEVEL_INTERFACE -#define XDG_TOPLEVEL_INTERFACE -/** - * @page page_iface_xdg_toplevel xdg_toplevel - * @section page_iface_xdg_toplevel_desc Description - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by perfoming a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - * @section page_iface_xdg_toplevel_api API - * See @ref iface_xdg_toplevel. - */ -/** - * @defgroup iface_xdg_toplevel The xdg_toplevel interface - * - * This interface defines an xdg_surface role which allows a surface to, - * among other things, set window-like properties such as maximize, - * fullscreen, and minimize, set application-specific metadata like title and - * id, and well as trigger user interactive operations such as interactive - * resize and move. - * - * Unmapping an xdg_toplevel means that the surface cannot be shown - * by the compositor until it is explicitly mapped again. - * All active operations (e.g., move, resize) are canceled and all - * attributes (e.g. title, state, stacking, ...) are discarded for - * an xdg_toplevel surface when it is unmapped. The xdg_toplevel returns to - * the state it had right after xdg_surface.get_toplevel. The client - * can re-map the toplevel by perfoming a commit without any buffer - * attached, waiting for a configure event and handling it as usual (see - * xdg_surface description). - * - * Attaching a null buffer to a toplevel unmaps the surface. - */ -extern const struct wl_interface xdg_toplevel_interface; -#endif -#ifndef XDG_POPUP_INTERFACE -#define XDG_POPUP_INTERFACE -/** - * @page page_iface_xdg_popup xdg_popup - * @section page_iface_xdg_popup_desc Description - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - * @section page_iface_xdg_popup_api API - * See @ref iface_xdg_popup. - */ -/** - * @defgroup iface_xdg_popup The xdg_popup interface - * - * A popup surface is a short-lived, temporary surface. It can be used to - * implement for example menus, popovers, tooltips and other similar user - * interface concepts. - * - * A popup can be made to take an explicit grab. See xdg_popup.grab for - * details. - * - * When the popup is dismissed, a popup_done event will be sent out, and at - * the same time the surface will be unmapped. See the xdg_popup.popup_done - * event for details. - * - * Explicitly destroying the xdg_popup object will also dismiss the popup and - * unmap the surface. Clients that want to dismiss the popup when another - * surface of their own is clicked should dismiss the popup using the destroy - * request. - * - * A newly created xdg_popup will be stacked on top of all previously created - * xdg_popup surfaces associated with the same xdg_toplevel. - * - * The parent of an xdg_popup must be mapped (see the xdg_surface - * description) before the xdg_popup itself. - * - * The client must call wl_surface.commit on the corresponding wl_surface - * for the xdg_popup state to take effect. - */ -extern const struct wl_interface xdg_popup_interface; -#endif - -#ifndef XDG_WM_BASE_ERROR_ENUM -#define XDG_WM_BASE_ERROR_ENUM -enum xdg_wm_base_error { - /** - * given wl_surface has another role - */ - XDG_WM_BASE_ERROR_ROLE = 0, - /** - * xdg_wm_base was destroyed before children - */ - XDG_WM_BASE_ERROR_DEFUNCT_SURFACES = 1, - /** - * the client tried to map or destroy a non-topmost popup - */ - XDG_WM_BASE_ERROR_NOT_THE_TOPMOST_POPUP = 2, - /** - * the client specified an invalid popup parent surface - */ - XDG_WM_BASE_ERROR_INVALID_POPUP_PARENT = 3, - /** - * the client provided an invalid surface state - */ - XDG_WM_BASE_ERROR_INVALID_SURFACE_STATE = 4, - /** - * the client provided an invalid positioner - */ - XDG_WM_BASE_ERROR_INVALID_POSITIONER = 5, -}; -#endif /* XDG_WM_BASE_ERROR_ENUM */ - -/** - * @ingroup iface_xdg_wm_base - * @struct xdg_wm_base_listener - */ -struct xdg_wm_base_listener { - /** - * check if the client is alive - * - * The ping event asks the client if it's still alive. Pass the - * serial specified in the event back to the compositor by sending - * a "pong" request back with the specified serial. See - * xdg_wm_base.pong. - * - * Compositors can use this to determine if the client is still - * alive. It's unspecified what will happen if the client doesn't - * respond to the ping request, or in what timeframe. Clients - * should try to respond in a reasonable amount of time. - * - * A compositor is free to ping in any way it wants, but a client - * must always respond to any xdg_wm_base object it created. - * @param serial pass this to the pong request - */ - void (*ping)(void *data, - struct xdg_wm_base *xdg_wm_base, - uint32_t serial); -}; - -/** - * @ingroup iface_xdg_wm_base - */ -static inline int -xdg_wm_base_add_listener(struct xdg_wm_base *xdg_wm_base, - const struct xdg_wm_base_listener *listener, void *data) -{ - return wl_proxy_add_listener((struct wl_proxy *) xdg_wm_base, - (void (**)(void)) listener, data); -} - -#define XDG_WM_BASE_DESTROY 0 -#define XDG_WM_BASE_CREATE_POSITIONER 1 -#define XDG_WM_BASE_GET_XDG_SURFACE 2 -#define XDG_WM_BASE_PONG 3 - -/** - * @ingroup iface_xdg_wm_base - */ -#define XDG_WM_BASE_PING_SINCE_VERSION 1 - -/** - * @ingroup iface_xdg_wm_base - */ -#define XDG_WM_BASE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ -#define XDG_WM_BASE_CREATE_POSITIONER_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ -#define XDG_WM_BASE_GET_XDG_SURFACE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_wm_base - */ -#define XDG_WM_BASE_PONG_SINCE_VERSION 1 - -/** @ingroup iface_xdg_wm_base */ -static inline void -xdg_wm_base_set_user_data(struct xdg_wm_base *xdg_wm_base, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) xdg_wm_base, user_data); -} - -/** @ingroup iface_xdg_wm_base */ -static inline void * -xdg_wm_base_get_user_data(struct xdg_wm_base *xdg_wm_base) -{ - return wl_proxy_get_user_data((struct wl_proxy *) xdg_wm_base); -} - -static inline uint32_t -xdg_wm_base_get_version(struct xdg_wm_base *xdg_wm_base) -{ - return wl_proxy_get_version((struct wl_proxy *) xdg_wm_base); -} - -/** - * @ingroup iface_xdg_wm_base - * - * Destroy this xdg_wm_base object. - * - * Destroying a bound xdg_wm_base object while there are surfaces - * still alive created by this xdg_wm_base object instance is illegal - * and will result in a protocol error. - */ -static inline void -xdg_wm_base_destroy(struct xdg_wm_base *xdg_wm_base) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_wm_base, - XDG_WM_BASE_DESTROY); - - wl_proxy_destroy((struct wl_proxy *) xdg_wm_base); -} - -/** - * @ingroup iface_xdg_wm_base - * - * Create a positioner object. A positioner object is used to position - * surfaces relative to some parent surface. See the interface description - * and xdg_surface.get_popup for details. - */ -static inline struct xdg_positioner * -xdg_wm_base_create_positioner(struct xdg_wm_base *xdg_wm_base) -{ - struct wl_proxy *id; - - id = wl_proxy_marshal_constructor((struct wl_proxy *) xdg_wm_base, - XDG_WM_BASE_CREATE_POSITIONER, &xdg_positioner_interface, NULL); - - return (struct xdg_positioner *) id; -} - -/** - * @ingroup iface_xdg_wm_base - * - * This creates an xdg_surface for the given surface. While xdg_surface - * itself is not a role, the corresponding surface may only be assigned - * a role extending xdg_surface, such as xdg_toplevel or xdg_popup. - * - * This creates an xdg_surface for the given surface. An xdg_surface is - * used as basis to define a role to a given surface, such as xdg_toplevel - * or xdg_popup. It also manages functionality shared between xdg_surface - * based surface roles. - * - * See the documentation of xdg_surface for more details about what an - * xdg_surface is and how it is used. - */ -static inline struct xdg_surface * -xdg_wm_base_get_xdg_surface(struct xdg_wm_base *xdg_wm_base, struct wl_surface *surface) -{ - struct wl_proxy *id; - - id = wl_proxy_marshal_constructor((struct wl_proxy *) xdg_wm_base, - XDG_WM_BASE_GET_XDG_SURFACE, &xdg_surface_interface, NULL, surface); - - return (struct xdg_surface *) id; -} - -/** - * @ingroup iface_xdg_wm_base - * - * A client must respond to a ping event with a pong request or - * the client may be deemed unresponsive. See xdg_wm_base.ping. - */ -static inline void -xdg_wm_base_pong(struct xdg_wm_base *xdg_wm_base, uint32_t serial) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_wm_base, - XDG_WM_BASE_PONG, serial); -} - -#ifndef XDG_POSITIONER_ERROR_ENUM -#define XDG_POSITIONER_ERROR_ENUM -enum xdg_positioner_error { - /** - * invalid input provided - */ - XDG_POSITIONER_ERROR_INVALID_INPUT = 0, -}; -#endif /* XDG_POSITIONER_ERROR_ENUM */ - -#ifndef XDG_POSITIONER_ANCHOR_ENUM -#define XDG_POSITIONER_ANCHOR_ENUM -enum xdg_positioner_anchor { - XDG_POSITIONER_ANCHOR_NONE = 0, - XDG_POSITIONER_ANCHOR_TOP = 1, - XDG_POSITIONER_ANCHOR_BOTTOM = 2, - XDG_POSITIONER_ANCHOR_LEFT = 3, - XDG_POSITIONER_ANCHOR_RIGHT = 4, - XDG_POSITIONER_ANCHOR_TOP_LEFT = 5, - XDG_POSITIONER_ANCHOR_BOTTOM_LEFT = 6, - XDG_POSITIONER_ANCHOR_TOP_RIGHT = 7, - XDG_POSITIONER_ANCHOR_BOTTOM_RIGHT = 8, -}; -#endif /* XDG_POSITIONER_ANCHOR_ENUM */ - -#ifndef XDG_POSITIONER_GRAVITY_ENUM -#define XDG_POSITIONER_GRAVITY_ENUM -enum xdg_positioner_gravity { - XDG_POSITIONER_GRAVITY_NONE = 0, - XDG_POSITIONER_GRAVITY_TOP = 1, - XDG_POSITIONER_GRAVITY_BOTTOM = 2, - XDG_POSITIONER_GRAVITY_LEFT = 3, - XDG_POSITIONER_GRAVITY_RIGHT = 4, - XDG_POSITIONER_GRAVITY_TOP_LEFT = 5, - XDG_POSITIONER_GRAVITY_BOTTOM_LEFT = 6, - XDG_POSITIONER_GRAVITY_TOP_RIGHT = 7, - XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT = 8, -}; -#endif /* XDG_POSITIONER_GRAVITY_ENUM */ - -#ifndef XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM -#define XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM -/** - * @ingroup iface_xdg_positioner - * vertically resize the surface - * - * Resize the surface vertically so that it is completely unconstrained. - */ -enum xdg_positioner_constraint_adjustment { - XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_NONE = 0, - XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X = 1, - XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_Y = 2, - XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_X = 4, - XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y = 8, - XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_X = 16, - XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_RESIZE_Y = 32, -}; -#endif /* XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_ENUM */ - -#define XDG_POSITIONER_DESTROY 0 -#define XDG_POSITIONER_SET_SIZE 1 -#define XDG_POSITIONER_SET_ANCHOR_RECT 2 -#define XDG_POSITIONER_SET_ANCHOR 3 -#define XDG_POSITIONER_SET_GRAVITY 4 -#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT 5 -#define XDG_POSITIONER_SET_OFFSET 6 -#define XDG_POSITIONER_SET_REACTIVE 7 -#define XDG_POSITIONER_SET_PARENT_SIZE 8 -#define XDG_POSITIONER_SET_PARENT_CONFIGURE 9 - - -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_SET_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_SET_ANCHOR_RECT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_SET_ANCHOR_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_SET_GRAVITY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_SET_OFFSET_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_SET_REACTIVE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_SET_PARENT_SIZE_SINCE_VERSION 3 -/** - * @ingroup iface_xdg_positioner - */ -#define XDG_POSITIONER_SET_PARENT_CONFIGURE_SINCE_VERSION 3 - -/** @ingroup iface_xdg_positioner */ -static inline void -xdg_positioner_set_user_data(struct xdg_positioner *xdg_positioner, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) xdg_positioner, user_data); -} - -/** @ingroup iface_xdg_positioner */ -static inline void * -xdg_positioner_get_user_data(struct xdg_positioner *xdg_positioner) -{ - return wl_proxy_get_user_data((struct wl_proxy *) xdg_positioner); -} - -static inline uint32_t -xdg_positioner_get_version(struct xdg_positioner *xdg_positioner) -{ - return wl_proxy_get_version((struct wl_proxy *) xdg_positioner); -} - -/** - * @ingroup iface_xdg_positioner - * - * Notify the compositor that the xdg_positioner will no longer be used. - */ -static inline void -xdg_positioner_destroy(struct xdg_positioner *xdg_positioner) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_DESTROY); - - wl_proxy_destroy((struct wl_proxy *) xdg_positioner); -} - -/** - * @ingroup iface_xdg_positioner - * - * Set the size of the surface that is to be positioned with the positioner - * object. The size is in surface-local coordinates and corresponds to the - * window geometry. See xdg_surface.set_window_geometry. - * - * If a zero or negative size is set the invalid_input error is raised. - */ -static inline void -xdg_positioner_set_size(struct xdg_positioner *xdg_positioner, int32_t width, int32_t height) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_SET_SIZE, width, height); -} - -/** - * @ingroup iface_xdg_positioner - * - * Specify the anchor rectangle within the parent surface that the child - * surface will be placed relative to. The rectangle is relative to the - * window geometry as defined by xdg_surface.set_window_geometry of the - * parent surface. - * - * When the xdg_positioner object is used to position a child surface, the - * anchor rectangle may not extend outside the window geometry of the - * positioned child's parent surface. - * - * If a negative size is set the invalid_input error is raised. - */ -static inline void -xdg_positioner_set_anchor_rect(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y, int32_t width, int32_t height) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_SET_ANCHOR_RECT, x, y, width, height); -} - -/** - * @ingroup iface_xdg_positioner - * - * Defines the anchor point for the anchor rectangle. The specified anchor - * is used derive an anchor point that the child surface will be - * positioned relative to. If a corner anchor is set (e.g. 'top_left' or - * 'bottom_right'), the anchor point will be at the specified corner; - * otherwise, the derived anchor point will be centered on the specified - * edge, or in the center of the anchor rectangle if no edge is specified. - */ -static inline void -xdg_positioner_set_anchor(struct xdg_positioner *xdg_positioner, uint32_t anchor) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_SET_ANCHOR, anchor); -} - -/** - * @ingroup iface_xdg_positioner - * - * Defines in what direction a surface should be positioned, relative to - * the anchor point of the parent surface. If a corner gravity is - * specified (e.g. 'bottom_right' or 'top_left'), then the child surface - * will be placed towards the specified gravity; otherwise, the child - * surface will be centered over the anchor point on any axis that had no - * gravity specified. - */ -static inline void -xdg_positioner_set_gravity(struct xdg_positioner *xdg_positioner, uint32_t gravity) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_SET_GRAVITY, gravity); -} - -/** - * @ingroup iface_xdg_positioner - * - * Specify how the window should be positioned if the originally intended - * position caused the surface to be constrained, meaning at least - * partially outside positioning boundaries set by the compositor. The - * adjustment is set by constructing a bitmask describing the adjustment to - * be made when the surface is constrained on that axis. - * - * If no bit for one axis is set, the compositor will assume that the child - * surface should not change its position on that axis when constrained. - * - * If more than one bit for one axis is set, the order of how adjustments - * are applied is specified in the corresponding adjustment descriptions. - * - * The default adjustment is none. - */ -static inline void -xdg_positioner_set_constraint_adjustment(struct xdg_positioner *xdg_positioner, uint32_t constraint_adjustment) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_SET_CONSTRAINT_ADJUSTMENT, constraint_adjustment); -} - -/** - * @ingroup iface_xdg_positioner - * - * Specify the surface position offset relative to the position of the - * anchor on the anchor rectangle and the anchor on the surface. For - * example if the anchor of the anchor rectangle is at (x, y), the surface - * has the gravity bottom|right, and the offset is (ox, oy), the calculated - * surface position will be (x + ox, y + oy). The offset position of the - * surface is the one used for constraint testing. See - * set_constraint_adjustment. - * - * An example use case is placing a popup menu on top of a user interface - * element, while aligning the user interface element of the parent surface - * with some user interface element placed somewhere in the popup surface. - */ -static inline void -xdg_positioner_set_offset(struct xdg_positioner *xdg_positioner, int32_t x, int32_t y) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_SET_OFFSET, x, y); -} - -/** - * @ingroup iface_xdg_positioner - * - * When set reactive, the surface is reconstrained if the conditions used - * for constraining changed, e.g. the parent window moved. - * - * If the conditions changed and the popup was reconstrained, an - * xdg_popup.configure event is sent with updated geometry, followed by an - * xdg_surface.configure event. - */ -static inline void -xdg_positioner_set_reactive(struct xdg_positioner *xdg_positioner) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_SET_REACTIVE); -} - -/** - * @ingroup iface_xdg_positioner - * - * Set the parent window geometry the compositor should use when - * positioning the popup. The compositor may use this information to - * determine the future state the popup should be constrained using. If - * this doesn't match the dimension of the parent the popup is eventually - * positioned against, the behavior is undefined. - * - * The arguments are given in the surface-local coordinate space. - */ -static inline void -xdg_positioner_set_parent_size(struct xdg_positioner *xdg_positioner, int32_t parent_width, int32_t parent_height) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_SET_PARENT_SIZE, parent_width, parent_height); -} - -/** - * @ingroup iface_xdg_positioner - * - * Set the serial of an xdg_surface.configure event this positioner will be - * used in response to. The compositor may use this information together - * with set_parent_size to determine what future state the popup should be - * constrained using. - */ -static inline void -xdg_positioner_set_parent_configure(struct xdg_positioner *xdg_positioner, uint32_t serial) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_positioner, - XDG_POSITIONER_SET_PARENT_CONFIGURE, serial); -} - -#ifndef XDG_SURFACE_ERROR_ENUM -#define XDG_SURFACE_ERROR_ENUM -enum xdg_surface_error { - XDG_SURFACE_ERROR_NOT_CONSTRUCTED = 1, - XDG_SURFACE_ERROR_ALREADY_CONSTRUCTED = 2, - XDG_SURFACE_ERROR_UNCONFIGURED_BUFFER = 3, -}; -#endif /* XDG_SURFACE_ERROR_ENUM */ - -/** - * @ingroup iface_xdg_surface - * @struct xdg_surface_listener - */ -struct xdg_surface_listener { - /** - * suggest a surface change - * - * The configure event marks the end of a configure sequence. A - * configure sequence is a set of one or more events configuring - * the state of the xdg_surface, including the final - * xdg_surface.configure event. - * - * Where applicable, xdg_surface surface roles will during a - * configure sequence extend this event as a latched state sent as - * events before the xdg_surface.configure event. Such events - * should be considered to make up a set of atomically applied - * configuration states, where the xdg_surface.configure commits - * the accumulated state. - * - * Clients should arrange their surface for the new states, and - * then send an ack_configure request with the serial sent in this - * configure event at some point before committing the new surface. - * - * If the client receives multiple configure events before it can - * respond to one, it is free to discard all but the last event it - * received. - * @param serial serial of the configure event - */ - void (*configure)(void *data, - struct xdg_surface *xdg_surface, - uint32_t serial); -}; - -/** - * @ingroup iface_xdg_surface - */ -static inline int -xdg_surface_add_listener(struct xdg_surface *xdg_surface, - const struct xdg_surface_listener *listener, void *data) -{ - return wl_proxy_add_listener((struct wl_proxy *) xdg_surface, - (void (**)(void)) listener, data); -} - -#define XDG_SURFACE_DESTROY 0 -#define XDG_SURFACE_GET_TOPLEVEL 1 -#define XDG_SURFACE_GET_POPUP 2 -#define XDG_SURFACE_SET_WINDOW_GEOMETRY 3 -#define XDG_SURFACE_ACK_CONFIGURE 4 - -/** - * @ingroup iface_xdg_surface - */ -#define XDG_SURFACE_CONFIGURE_SINCE_VERSION 1 - -/** - * @ingroup iface_xdg_surface - */ -#define XDG_SURFACE_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ -#define XDG_SURFACE_GET_TOPLEVEL_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ -#define XDG_SURFACE_GET_POPUP_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ -#define XDG_SURFACE_SET_WINDOW_GEOMETRY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_surface - */ -#define XDG_SURFACE_ACK_CONFIGURE_SINCE_VERSION 1 - -/** @ingroup iface_xdg_surface */ -static inline void -xdg_surface_set_user_data(struct xdg_surface *xdg_surface, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) xdg_surface, user_data); -} - -/** @ingroup iface_xdg_surface */ -static inline void * -xdg_surface_get_user_data(struct xdg_surface *xdg_surface) -{ - return wl_proxy_get_user_data((struct wl_proxy *) xdg_surface); -} - -static inline uint32_t -xdg_surface_get_version(struct xdg_surface *xdg_surface) -{ - return wl_proxy_get_version((struct wl_proxy *) xdg_surface); -} - -/** - * @ingroup iface_xdg_surface - * - * Destroy the xdg_surface object. An xdg_surface must only be destroyed - * after its role object has been destroyed. - */ -static inline void -xdg_surface_destroy(struct xdg_surface *xdg_surface) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_surface, - XDG_SURFACE_DESTROY); - - wl_proxy_destroy((struct wl_proxy *) xdg_surface); -} - -/** - * @ingroup iface_xdg_surface - * - * This creates an xdg_toplevel object for the given xdg_surface and gives - * the associated wl_surface the xdg_toplevel role. - * - * See the documentation of xdg_toplevel for more details about what an - * xdg_toplevel is and how it is used. - */ -static inline struct xdg_toplevel * -xdg_surface_get_toplevel(struct xdg_surface *xdg_surface) -{ - struct wl_proxy *id; - - id = wl_proxy_marshal_constructor((struct wl_proxy *) xdg_surface, - XDG_SURFACE_GET_TOPLEVEL, &xdg_toplevel_interface, NULL); - - return (struct xdg_toplevel *) id; -} - -/** - * @ingroup iface_xdg_surface - * - * This creates an xdg_popup object for the given xdg_surface and gives - * the associated wl_surface the xdg_popup role. - * - * If null is passed as a parent, a parent surface must be specified using - * some other protocol, before committing the initial state. - * - * See the documentation of xdg_popup for more details about what an - * xdg_popup is and how it is used. - */ -static inline struct xdg_popup * -xdg_surface_get_popup(struct xdg_surface *xdg_surface, struct xdg_surface *parent, struct xdg_positioner *positioner) -{ - struct wl_proxy *id; - - id = wl_proxy_marshal_constructor((struct wl_proxy *) xdg_surface, - XDG_SURFACE_GET_POPUP, &xdg_popup_interface, NULL, parent, positioner); - - return (struct xdg_popup *) id; -} - -/** - * @ingroup iface_xdg_surface - * - * The window geometry of a surface is its "visible bounds" from the - * user's perspective. Client-side decorations often have invisible - * portions like drop-shadows which should be ignored for the - * purposes of aligning, placing and constraining windows. - * - * The window geometry is double buffered, and will be applied at the - * time wl_surface.commit of the corresponding wl_surface is called. - * - * When maintaining a position, the compositor should treat the (x, y) - * coordinate of the window geometry as the top left corner of the window. - * A client changing the (x, y) window geometry coordinate should in - * general not alter the position of the window. - * - * Once the window geometry of the surface is set, it is not possible to - * unset it, and it will remain the same until set_window_geometry is - * called again, even if a new subsurface or buffer is attached. - * - * If never set, the value is the full bounds of the surface, - * including any subsurfaces. This updates dynamically on every - * commit. This unset is meant for extremely simple clients. - * - * The arguments are given in the surface-local coordinate space of - * the wl_surface associated with this xdg_surface. - * - * The width and height must be greater than zero. Setting an invalid size - * will raise an error. When applied, the effective window geometry will be - * the set window geometry clamped to the bounding rectangle of the - * combined geometry of the surface of the xdg_surface and the associated - * subsurfaces. - */ -static inline void -xdg_surface_set_window_geometry(struct xdg_surface *xdg_surface, int32_t x, int32_t y, int32_t width, int32_t height) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_surface, - XDG_SURFACE_SET_WINDOW_GEOMETRY, x, y, width, height); -} - -/** - * @ingroup iface_xdg_surface - * - * When a configure event is received, if a client commits the - * surface in response to the configure event, then the client - * must make an ack_configure request sometime before the commit - * request, passing along the serial of the configure event. - * - * For instance, for toplevel surfaces the compositor might use this - * information to move a surface to the top left only when the client has - * drawn itself for the maximized or fullscreen state. - * - * If the client receives multiple configure events before it - * can respond to one, it only has to ack the last configure event. - * - * A client is not required to commit immediately after sending - * an ack_configure request - it may even ack_configure several times - * before its next surface commit. - * - * A client may send multiple ack_configure requests before committing, but - * only the last request sent before a commit indicates which configure - * event the client really is responding to. - */ -static inline void -xdg_surface_ack_configure(struct xdg_surface *xdg_surface, uint32_t serial) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_surface, - XDG_SURFACE_ACK_CONFIGURE, serial); -} - -#ifndef XDG_TOPLEVEL_RESIZE_EDGE_ENUM -#define XDG_TOPLEVEL_RESIZE_EDGE_ENUM -/** - * @ingroup iface_xdg_toplevel - * edge values for resizing - * - * These values are used to indicate which edge of a surface - * is being dragged in a resize operation. - */ -enum xdg_toplevel_resize_edge { - XDG_TOPLEVEL_RESIZE_EDGE_NONE = 0, - XDG_TOPLEVEL_RESIZE_EDGE_TOP = 1, - XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM = 2, - XDG_TOPLEVEL_RESIZE_EDGE_LEFT = 4, - XDG_TOPLEVEL_RESIZE_EDGE_TOP_LEFT = 5, - XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_LEFT = 6, - XDG_TOPLEVEL_RESIZE_EDGE_RIGHT = 8, - XDG_TOPLEVEL_RESIZE_EDGE_TOP_RIGHT = 9, - XDG_TOPLEVEL_RESIZE_EDGE_BOTTOM_RIGHT = 10, -}; -#endif /* XDG_TOPLEVEL_RESIZE_EDGE_ENUM */ - -#ifndef XDG_TOPLEVEL_STATE_ENUM -#define XDG_TOPLEVEL_STATE_ENUM -/** - * @ingroup iface_xdg_toplevel - * the surface is tiled - * - * The window is currently in a tiled layout and the bottom edge is - * considered to be adjacent to another part of the tiling grid. - */ -enum xdg_toplevel_state { - /** - * the surface is maximized - */ - XDG_TOPLEVEL_STATE_MAXIMIZED = 1, - /** - * the surface is fullscreen - */ - XDG_TOPLEVEL_STATE_FULLSCREEN = 2, - /** - * the surface is being resized - */ - XDG_TOPLEVEL_STATE_RESIZING = 3, - /** - * the surface is now activated - */ - XDG_TOPLEVEL_STATE_ACTIVATED = 4, - /** - * @since 2 - */ - XDG_TOPLEVEL_STATE_TILED_LEFT = 5, - /** - * @since 2 - */ - XDG_TOPLEVEL_STATE_TILED_RIGHT = 6, - /** - * @since 2 - */ - XDG_TOPLEVEL_STATE_TILED_TOP = 7, - /** - * @since 2 - */ - XDG_TOPLEVEL_STATE_TILED_BOTTOM = 8, -}; -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_STATE_TILED_LEFT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_STATE_TILED_RIGHT_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_STATE_TILED_TOP_SINCE_VERSION 2 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_STATE_TILED_BOTTOM_SINCE_VERSION 2 -#endif /* XDG_TOPLEVEL_STATE_ENUM */ - -/** - * @ingroup iface_xdg_toplevel - * @struct xdg_toplevel_listener - */ -struct xdg_toplevel_listener { - /** - * suggest a surface change - * - * This configure event asks the client to resize its toplevel - * surface or to change its state. The configured state should not - * be applied immediately. See xdg_surface.configure for details. - * - * The width and height arguments specify a hint to the window - * about how its surface should be resized in window geometry - * coordinates. See set_window_geometry. - * - * If the width or height arguments are zero, it means the client - * should decide its own window dimension. This may happen when the - * compositor needs to configure the state of the surface but - * doesn't have any information about any previous or expected - * dimension. - * - * The states listed in the event specify how the width/height - * arguments should be interpreted, and possibly how it should be - * drawn. - * - * Clients must send an ack_configure in response to this event. - * See xdg_surface.configure and xdg_surface.ack_configure for - * details. - */ - void (*configure)(void *data, - struct xdg_toplevel *xdg_toplevel, - int32_t width, - int32_t height, - struct wl_array *states); - /** - * surface wants to be closed - * - * The close event is sent by the compositor when the user wants - * the surface to be closed. This should be equivalent to the user - * clicking the close button in client-side decorations, if your - * application has any. - * - * This is only a request that the user intends to close the - * window. The client may choose to ignore this request, or show a - * dialog to ask the user to save their data, etc. - */ - void (*close)(void *data, - struct xdg_toplevel *xdg_toplevel); -}; - -/** - * @ingroup iface_xdg_toplevel - */ -static inline int -xdg_toplevel_add_listener(struct xdg_toplevel *xdg_toplevel, - const struct xdg_toplevel_listener *listener, void *data) -{ - return wl_proxy_add_listener((struct wl_proxy *) xdg_toplevel, - (void (**)(void)) listener, data); -} - -#define XDG_TOPLEVEL_DESTROY 0 -#define XDG_TOPLEVEL_SET_PARENT 1 -#define XDG_TOPLEVEL_SET_TITLE 2 -#define XDG_TOPLEVEL_SET_APP_ID 3 -#define XDG_TOPLEVEL_SHOW_WINDOW_MENU 4 -#define XDG_TOPLEVEL_MOVE 5 -#define XDG_TOPLEVEL_RESIZE 6 -#define XDG_TOPLEVEL_SET_MAX_SIZE 7 -#define XDG_TOPLEVEL_SET_MIN_SIZE 8 -#define XDG_TOPLEVEL_SET_MAXIMIZED 9 -#define XDG_TOPLEVEL_UNSET_MAXIMIZED 10 -#define XDG_TOPLEVEL_SET_FULLSCREEN 11 -#define XDG_TOPLEVEL_UNSET_FULLSCREEN 12 -#define XDG_TOPLEVEL_SET_MINIMIZED 13 - -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_CLOSE_SINCE_VERSION 1 - -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_SET_PARENT_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_SET_TITLE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_SET_APP_ID_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_SHOW_WINDOW_MENU_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_MOVE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_RESIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_SET_MAX_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_SET_MIN_SIZE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_SET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_UNSET_MAXIMIZED_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_SET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_UNSET_FULLSCREEN_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_toplevel - */ -#define XDG_TOPLEVEL_SET_MINIMIZED_SINCE_VERSION 1 - -/** @ingroup iface_xdg_toplevel */ -static inline void -xdg_toplevel_set_user_data(struct xdg_toplevel *xdg_toplevel, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) xdg_toplevel, user_data); -} - -/** @ingroup iface_xdg_toplevel */ -static inline void * -xdg_toplevel_get_user_data(struct xdg_toplevel *xdg_toplevel) -{ - return wl_proxy_get_user_data((struct wl_proxy *) xdg_toplevel); -} - -static inline uint32_t -xdg_toplevel_get_version(struct xdg_toplevel *xdg_toplevel) -{ - return wl_proxy_get_version((struct wl_proxy *) xdg_toplevel); -} - -/** - * @ingroup iface_xdg_toplevel - * - * This request destroys the role surface and unmaps the surface; - * see "Unmapping" behavior in interface section for details. - */ -static inline void -xdg_toplevel_destroy(struct xdg_toplevel *xdg_toplevel) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_DESTROY); - - wl_proxy_destroy((struct wl_proxy *) xdg_toplevel); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Set the "parent" of this surface. This surface should be stacked - * above the parent surface and all other ancestor surfaces. - * - * Parent windows should be set on dialogs, toolboxes, or other - * "auxiliary" surfaces, so that the parent is raised when the dialog - * is raised. - * - * Setting a null parent for a child window removes any parent-child - * relationship for the child. Setting a null parent for a window which - * currently has no parent is a no-op. - * - * If the parent is unmapped then its children are managed as - * though the parent of the now-unmapped parent has become the - * parent of this surface. If no parent exists for the now-unmapped - * parent then the children are managed as though they have no - * parent surface. - */ -static inline void -xdg_toplevel_set_parent(struct xdg_toplevel *xdg_toplevel, struct xdg_toplevel *parent) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_SET_PARENT, parent); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Set a short title for the surface. - * - * This string may be used to identify the surface in a task bar, - * window list, or other user interface elements provided by the - * compositor. - * - * The string must be encoded in UTF-8. - */ -static inline void -xdg_toplevel_set_title(struct xdg_toplevel *xdg_toplevel, const char *title) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_SET_TITLE, title); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Set an application identifier for the surface. - * - * The app ID identifies the general class of applications to which - * the surface belongs. The compositor can use this to group multiple - * surfaces together, or to determine how to launch a new application. - * - * For D-Bus activatable applications, the app ID is used as the D-Bus - * service name. - * - * The compositor shell will try to group application surfaces together - * by their app ID. As a best practice, it is suggested to select app - * ID's that match the basename of the application's .desktop file. - * For example, "org.freedesktop.FooViewer" where the .desktop file is - * "org.freedesktop.FooViewer.desktop". - * - * Like other properties, a set_app_id request can be sent after the - * xdg_toplevel has been mapped to update the property. - * - * See the desktop-entry specification [0] for more details on - * application identifiers and how they relate to well-known D-Bus - * names and .desktop files. - * - * [0] http://standards.freedesktop.org/desktop-entry-spec/ - */ -static inline void -xdg_toplevel_set_app_id(struct xdg_toplevel *xdg_toplevel, const char *app_id) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_SET_APP_ID, app_id); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Clients implementing client-side decorations might want to show - * a context menu when right-clicking on the decorations, giving the - * user a menu that they can use to maximize or minimize the window. - * - * This request asks the compositor to pop up such a window menu at - * the given position, relative to the local surface coordinates of - * the parent surface. There are no guarantees as to what menu items - * the window menu contains. - * - * This request must be used in response to some sort of user action - * like a button press, key press, or touch down event. - */ -static inline void -xdg_toplevel_show_window_menu(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, int32_t x, int32_t y) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_SHOW_WINDOW_MENU, seat, serial, x, y); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Start an interactive, user-driven move of the surface. - * - * This request must be used in response to some sort of user action - * like a button press, key press, or touch down event. The passed - * serial is used to determine the type of interactive move (touch, - * pointer, etc). - * - * The server may ignore move requests depending on the state of - * the surface (e.g. fullscreen or maximized), or if the passed serial - * is no longer valid. - * - * If triggered, the surface will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the move. It is up to the - * compositor to visually indicate that the move is taking place, such as - * updating a pointer cursor, during the move. There is no guarantee - * that the device focus will return when the move is completed. - */ -static inline void -xdg_toplevel_move(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_MOVE, seat, serial); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Start a user-driven, interactive resize of the surface. - * - * This request must be used in response to some sort of user action - * like a button press, key press, or touch down event. The passed - * serial is used to determine the type of interactive resize (touch, - * pointer, etc). - * - * The server may ignore resize requests depending on the state of - * the surface (e.g. fullscreen or maximized). - * - * If triggered, the client will receive configure events with the - * "resize" state enum value and the expected sizes. See the "resize" - * enum value for more details about what is required. The client - * must also acknowledge configure events using "ack_configure". After - * the resize is completed, the client will receive another "configure" - * event without the resize state. - * - * If triggered, the surface also will lose the focus of the device - * (wl_pointer, wl_touch, etc) used for the resize. It is up to the - * compositor to visually indicate that the resize is taking place, - * such as updating a pointer cursor, during the resize. There is no - * guarantee that the device focus will return when the resize is - * completed. - * - * The edges parameter specifies how the surface should be resized, - * and is one of the values of the resize_edge enum. The compositor - * may use this information to update the surface position for - * example when dragging the top left corner. The compositor may also - * use this information to adapt its behavior, e.g. choose an - * appropriate cursor image. - */ -static inline void -xdg_toplevel_resize(struct xdg_toplevel *xdg_toplevel, struct wl_seat *seat, uint32_t serial, uint32_t edges) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_RESIZE, seat, serial, edges); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Set a maximum size for the window. - * - * The client can specify a maximum size so that the compositor does - * not try to configure the window beyond this size. - * - * The width and height arguments are in window geometry coordinates. - * See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get applied - * on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the maximum - * size. The compositor may decide to ignore the values set by the - * client and request a larger size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected maximum size in the given dimension. - * As a result, a client wishing to reset the maximum size - * to an unspecified state can use zero for width and height in the - * request. - * - * Requesting a maximum size to be smaller than the minimum size of - * a surface is illegal and will result in a protocol error. - * - * The width and height must be greater than or equal to zero. Using - * strictly negative values for width and height will result in a - * protocol error. - */ -static inline void -xdg_toplevel_set_max_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_SET_MAX_SIZE, width, height); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Set a minimum size for the window. - * - * The client can specify a minimum size so that the compositor does - * not try to configure the window below this size. - * - * The width and height arguments are in window geometry coordinates. - * See xdg_surface.set_window_geometry. - * - * Values set in this way are double-buffered. They will get applied - * on the next commit. - * - * The compositor can use this information to allow or disallow - * different states like maximize or fullscreen and draw accurate - * animations. - * - * Similarly, a tiling window manager may use this information to - * place and resize client windows in a more effective way. - * - * The client should not rely on the compositor to obey the minimum - * size. The compositor may decide to ignore the values set by the - * client and request a smaller size. - * - * If never set, or a value of zero in the request, means that the - * client has no expected minimum size in the given dimension. - * As a result, a client wishing to reset the minimum size - * to an unspecified state can use zero for width and height in the - * request. - * - * Requesting a minimum size to be larger than the maximum size of - * a surface is illegal and will result in a protocol error. - * - * The width and height must be greater than or equal to zero. Using - * strictly negative values for width and height will result in a - * protocol error. - */ -static inline void -xdg_toplevel_set_min_size(struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_SET_MIN_SIZE, width, height); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Maximize the surface. - * - * After requesting that the surface should be maximized, the compositor - * will respond by emitting a configure event. Whether this configure - * actually sets the window maximized is subject to compositor policies. - * The client must then update its content, drawing in the configured - * state. The client must also acknowledge the configure when committing - * the new content (see ack_configure). - * - * It is up to the compositor to decide how and where to maximize the - * surface, for example which output and what region of the screen should - * be used. - * - * If the surface was already maximized, the compositor will still emit - * a configure event with the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no direct - * effect. It may alter the state the surface is returned to when - * unmaximized unless overridden by the compositor. - */ -static inline void -xdg_toplevel_set_maximized(struct xdg_toplevel *xdg_toplevel) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_SET_MAXIMIZED); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Unmaximize the surface. - * - * After requesting that the surface should be unmaximized, the compositor - * will respond by emitting a configure event. Whether this actually - * un-maximizes the window is subject to compositor policies. - * If available and applicable, the compositor will include the window - * geometry dimensions the window had prior to being maximized in the - * configure event. The client must then update its content, drawing it in - * the configured state. The client must also acknowledge the configure - * when committing the new content (see ack_configure). - * - * It is up to the compositor to position the surface after it was - * unmaximized; usually the position the surface had before maximizing, if - * applicable. - * - * If the surface was already not maximized, the compositor will still - * emit a configure event without the "maximized" state. - * - * If the surface is in a fullscreen state, this request has no direct - * effect. It may alter the state the surface is returned to when - * unmaximized unless overridden by the compositor. - */ -static inline void -xdg_toplevel_unset_maximized(struct xdg_toplevel *xdg_toplevel) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_UNSET_MAXIMIZED); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Make the surface fullscreen. - * - * After requesting that the surface should be fullscreened, the - * compositor will respond by emitting a configure event. Whether the - * client is actually put into a fullscreen state is subject to compositor - * policies. The client must also acknowledge the configure when - * committing the new content (see ack_configure). - * - * The output passed by the request indicates the client's preference as - * to which display it should be set fullscreen on. If this value is NULL, - * it's up to the compositor to choose which display will be used to map - * this surface. - * - * If the surface doesn't cover the whole output, the compositor will - * position the surface in the center of the output and compensate with - * with border fill covering the rest of the output. The content of the - * border fill is undefined, but should be assumed to be in some way that - * attempts to blend into the surrounding area (e.g. solid black). - * - * If the fullscreened surface is not opaque, the compositor must make - * sure that other screen content not part of the same surface tree (made - * up of subsurfaces, popups or similarly coupled surfaces) are not - * visible below the fullscreened surface. - */ -static inline void -xdg_toplevel_set_fullscreen(struct xdg_toplevel *xdg_toplevel, struct wl_output *output) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_SET_FULLSCREEN, output); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Make the surface no longer fullscreen. - * - * After requesting that the surface should be unfullscreened, the - * compositor will respond by emitting a configure event. - * Whether this actually removes the fullscreen state of the client is - * subject to compositor policies. - * - * Making a surface unfullscreen sets states for the surface based on the following: - * * the state(s) it may have had before becoming fullscreen - * * any state(s) decided by the compositor - * * any state(s) requested by the client while the surface was fullscreen - * - * The compositor may include the previous window geometry dimensions in - * the configure event, if applicable. - * - * The client must also acknowledge the configure when committing the new - * content (see ack_configure). - */ -static inline void -xdg_toplevel_unset_fullscreen(struct xdg_toplevel *xdg_toplevel) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_UNSET_FULLSCREEN); -} - -/** - * @ingroup iface_xdg_toplevel - * - * Request that the compositor minimize your surface. There is no - * way to know if the surface is currently minimized, nor is there - * any way to unset minimization on this surface. - * - * If you are looking to throttle redrawing when minimized, please - * instead use the wl_surface.frame event for this, as this will - * also work with live previews on windows in Alt-Tab, Expose or - * similar compositor features. - */ -static inline void -xdg_toplevel_set_minimized(struct xdg_toplevel *xdg_toplevel) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_toplevel, - XDG_TOPLEVEL_SET_MINIMIZED); -} - -#ifndef XDG_POPUP_ERROR_ENUM -#define XDG_POPUP_ERROR_ENUM -enum xdg_popup_error { - /** - * tried to grab after being mapped - */ - XDG_POPUP_ERROR_INVALID_GRAB = 0, -}; -#endif /* XDG_POPUP_ERROR_ENUM */ - -/** - * @ingroup iface_xdg_popup - * @struct xdg_popup_listener - */ -struct xdg_popup_listener { - /** - * configure the popup surface - * - * This event asks the popup surface to configure itself given - * the configuration. The configured state should not be applied - * immediately. See xdg_surface.configure for details. - * - * The x and y arguments represent the position the popup was - * placed at given the xdg_positioner rule, relative to the upper - * left corner of the window geometry of the parent surface. - * - * For version 2 or older, the configure event for an xdg_popup is - * only ever sent once for the initial configuration. Starting with - * version 3, it may be sent again if the popup is setup with an - * xdg_positioner with set_reactive requested, or in response to - * xdg_popup.reposition requests. - * @param x x position relative to parent surface window geometry - * @param y y position relative to parent surface window geometry - * @param width window geometry width - * @param height window geometry height - */ - void (*configure)(void *data, - struct xdg_popup *xdg_popup, - int32_t x, - int32_t y, - int32_t width, - int32_t height); - /** - * popup interaction is done - * - * The popup_done event is sent out when a popup is dismissed by - * the compositor. The client should destroy the xdg_popup object - * at this point. - */ - void (*popup_done)(void *data, - struct xdg_popup *xdg_popup); - /** - * signal the completion of a repositioned request - * - * The repositioned event is sent as part of a popup - * configuration sequence, together with xdg_popup.configure and - * lastly xdg_surface.configure to notify the completion of a - * reposition request. - * - * The repositioned event is to notify about the completion of a - * xdg_popup.reposition request. The token argument is the token - * passed in the xdg_popup.reposition request. - * - * Immediately after this event is emitted, xdg_popup.configure and - * xdg_surface.configure will be sent with the updated size and - * position, as well as a new configure serial. - * - * The client should optionally update the content of the popup, - * but must acknowledge the new popup configuration for the new - * position to take effect. See xdg_surface.ack_configure for - * details. - * @param token reposition request token - * @since 3 - */ - void (*repositioned)(void *data, - struct xdg_popup *xdg_popup, - uint32_t token); -}; - -/** - * @ingroup iface_xdg_popup - */ -static inline int -xdg_popup_add_listener(struct xdg_popup *xdg_popup, - const struct xdg_popup_listener *listener, void *data) -{ - return wl_proxy_add_listener((struct wl_proxy *) xdg_popup, - (void (**)(void)) listener, data); -} - -#define XDG_POPUP_DESTROY 0 -#define XDG_POPUP_GRAB 1 -#define XDG_POPUP_REPOSITION 2 - -/** - * @ingroup iface_xdg_popup - */ -#define XDG_POPUP_CONFIGURE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ -#define XDG_POPUP_POPUP_DONE_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ -#define XDG_POPUP_REPOSITIONED_SINCE_VERSION 3 - -/** - * @ingroup iface_xdg_popup - */ -#define XDG_POPUP_DESTROY_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ -#define XDG_POPUP_GRAB_SINCE_VERSION 1 -/** - * @ingroup iface_xdg_popup - */ -#define XDG_POPUP_REPOSITION_SINCE_VERSION 3 - -/** @ingroup iface_xdg_popup */ -static inline void -xdg_popup_set_user_data(struct xdg_popup *xdg_popup, void *user_data) -{ - wl_proxy_set_user_data((struct wl_proxy *) xdg_popup, user_data); -} - -/** @ingroup iface_xdg_popup */ -static inline void * -xdg_popup_get_user_data(struct xdg_popup *xdg_popup) -{ - return wl_proxy_get_user_data((struct wl_proxy *) xdg_popup); -} - -static inline uint32_t -xdg_popup_get_version(struct xdg_popup *xdg_popup) -{ - return wl_proxy_get_version((struct wl_proxy *) xdg_popup); -} - -/** - * @ingroup iface_xdg_popup - * - * This destroys the popup. Explicitly destroying the xdg_popup - * object will also dismiss the popup, and unmap the surface. - * - * If this xdg_popup is not the "topmost" popup, a protocol error - * will be sent. - */ -static inline void -xdg_popup_destroy(struct xdg_popup *xdg_popup) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_popup, - XDG_POPUP_DESTROY); - - wl_proxy_destroy((struct wl_proxy *) xdg_popup); -} - -/** - * @ingroup iface_xdg_popup - * - * This request makes the created popup take an explicit grab. An explicit - * grab will be dismissed when the user dismisses the popup, or when the - * client destroys the xdg_popup. This can be done by the user clicking - * outside the surface, using the keyboard, or even locking the screen - * through closing the lid or a timeout. - * - * If the compositor denies the grab, the popup will be immediately - * dismissed. - * - * This request must be used in response to some sort of user action like a - * button press, key press, or touch down event. The serial number of the - * event should be passed as 'serial'. - * - * The parent of a grabbing popup must either be an xdg_toplevel surface or - * another xdg_popup with an explicit grab. If the parent is another - * xdg_popup it means that the popups are nested, with this popup now being - * the topmost popup. - * - * Nested popups must be destroyed in the reverse order they were created - * in, e.g. the only popup you are allowed to destroy at all times is the - * topmost one. - * - * When compositors choose to dismiss a popup, they may dismiss every - * nested grabbing popup as well. When a compositor dismisses popups, it - * will follow the same dismissing order as required from the client. - * - * The parent of a grabbing popup must either be another xdg_popup with an - * active explicit grab, or an xdg_popup or xdg_toplevel, if there are no - * explicit grabs already taken. - * - * If the topmost grabbing popup is destroyed, the grab will be returned to - * the parent of the popup, if that parent previously had an explicit grab. - * - * If the parent is a grabbing popup which has already been dismissed, this - * popup will be immediately dismissed. If the parent is a popup that did - * not take an explicit grab, an error will be raised. - * - * During a popup grab, the client owning the grab will receive pointer - * and touch events for all their surfaces as normal (similar to an - * "owner-events" grab in X11 parlance), while the top most grabbing popup - * will always have keyboard focus. - */ -static inline void -xdg_popup_grab(struct xdg_popup *xdg_popup, struct wl_seat *seat, uint32_t serial) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_popup, - XDG_POPUP_GRAB, seat, serial); -} - -/** - * @ingroup iface_xdg_popup - * - * Reposition an already-mapped popup. The popup will be placed given the - * details in the passed xdg_positioner object, and a - * xdg_popup.repositioned followed by xdg_popup.configure and - * xdg_surface.configure will be emitted in response. Any parameters set - * by the previous positioner will be discarded. - * - * The passed token will be sent in the corresponding - * xdg_popup.repositioned event. The new popup position will not take - * effect until the corresponding configure event is acknowledged by the - * client. See xdg_popup.repositioned for details. The token itself is - * opaque, and has no other special meaning. - * - * If multiple reposition requests are sent, the compositor may skip all - * but the last one. - * - * If the popup is repositioned in response to a configure event for its - * parent, the client should send an xdg_positioner.set_parent_configure - * and possibly an xdg_positioner.set_parent_size request to allow the - * compositor to properly constrain the popup. - * - * If the popup is repositioned together with a parent that is being - * resized, but not in response to a configure event, the client should - * send an xdg_positioner.set_parent_size request. - */ -static inline void -xdg_popup_reposition(struct xdg_popup *xdg_popup, struct xdg_positioner *positioner, uint32_t token) -{ - wl_proxy_marshal((struct wl_proxy *) xdg_popup, - XDG_POPUP_REPOSITION, positioner, token); -} - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/gio/app/window.go b/gio/app/window.go deleted file mode 100644 index 18bd2a8..0000000 --- a/gio/app/window.go +++ /dev/null @@ -1,981 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package app - -import ( - "errors" - "fmt" - "image" - "image/color" - "runtime" - "sync" - "time" - "unicode/utf8" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/font/gofont" - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/debug" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" - "github.com/p9c/p9/pkg/gel/gio/widget/material" -) - -// Option configures a window. -type Option func(unit.Metric, *Config) - -// Window represents an operating system window. -// -// The zero-value Window is useful; the GUI window is created and shown the first -// time the [Event] method is called. On iOS or Android, the first Window represents -// the window previously created by the platform. -// -// More than one Window is not supported on iOS, Android, WebAssembly. -type Window struct { - initialOpts []Option - initialActions []system.Action - - ctx context - gpu gpu.GPU - // timer tracks the delayed invalidate goroutine. - timer struct { - // quit is shuts down the goroutine. - quit chan struct{} - // update the invalidate time. - update chan time.Time - } - - animating bool - hasNextFrame bool - nextFrame time.Time - // viewport is the latest frame size with insets applied. - viewport image.Rectangle - // metric is the metric from the most recent frame. - metric unit.Metric - queue input.Router - cursor pointer.Cursor - decorations struct { - op.Ops - // enabled tracks the Decorated option as - // given to the Option method. It may differ - // from Config.Decorated depending on platform - // capability. - enabled bool - Config - height unit.Dp - currentHeight int - *material.Theme - *widget.Decorations - } - nocontext bool - // semantic data, lazily evaluated if requested by a backend to speed up - // the cases where semantic data is not needed. - semantic struct { - // uptodate tracks whether the fields below are up to date. - uptodate bool - root input.SemanticID - prevTree []input.SemanticNode - tree []input.SemanticNode - ids map[input.SemanticID]input.SemanticNode - } - imeState editorState - driver driver - // gpuErr tracks the GPU error that is to be reported when - // the window is closed. - gpuErr error - - // invMu protects mayInvalidate. - invMu sync.Mutex - mayInvalidate bool - - // coalesced tracks the most recent events waiting to be delivered - // to the client. - coalesced eventSummary - // frame tracks the most recent frame event. - lastFrame struct { - sync bool - size image.Point - off image.Point - deco op.CallOp - } -} - -type eventSummary struct { - wakeup bool - cfg *ConfigEvent - view *ViewEvent - frame *frameEvent - framePending bool - destroy *DestroyEvent -} - -type callbacks struct { - w *Window -} - -func decoHeightOpt(h unit.Dp) Option { - return func(m unit.Metric, c *Config) { - c.decoHeight = h - } -} - -func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops, sigChan chan<- struct{}) error { - signal := func() { - if sigChan != nil { - // We're done with frame, let the client continue. - sigChan <- struct{}{} - // Signal at most once. - sigChan = nil - } - } - defer signal() - for { - if w.gpu == nil && !w.nocontext { - var err error - if w.ctx == nil { - w.ctx, err = w.driver.NewContext() - if err != nil { - return err - } - sync = true - } - } - if sync && w.ctx != nil { - if err := w.ctx.Refresh(); err != nil { - if errors.Is(err, errOutOfDate) { - // Surface couldn't be created for transient reasons. Skip - // this frame and wait for the next. - return nil - } - w.destroyGPU() - if errors.Is(err, gpu.ErrDeviceLost) { - continue - } - return err - } - } - if w.ctx != nil { - if err := w.ctx.Lock(); err != nil { - w.destroyGPU() - return err - } - } - if w.gpu == nil && !w.nocontext { - gpu, err := gpu.New(w.ctx.API()) - if err != nil { - w.ctx.Unlock() - w.destroyGPU() - return err - } - w.gpu = gpu - } - if w.gpu != nil { - if err := w.frame(frame, size); err != nil { - w.ctx.Unlock() - if errors.Is(err, errOutOfDate) { - // GPU surface needs refreshing. - sync = true - continue - } - w.destroyGPU() - if errors.Is(err, gpu.ErrDeviceLost) { - continue - } - return err - } - } - w.queue.Frame(frame) - // Let the client continue as soon as possible, in particular before - // a potentially blocking Present. - signal() - var err error - if w.gpu != nil { - err = w.ctx.Present() - w.ctx.Unlock() - } - return err - } -} - -func (w *Window) frame(frame *op.Ops, viewport image.Point) error { - if runtime.GOOS == "js" { - // Use transparent black when Gio is embedded, to allow mixing of Gio and - // foreign content below. - w.gpu.Clear(color.NRGBA{A: 0x00, R: 0x00, G: 0x00, B: 0x00}) - } else { - w.gpu.Clear(color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff}) - } - target, err := w.ctx.RenderTarget() - if err != nil { - return err - } - return w.gpu.Frame(frame, target, viewport) -} - -func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) { - w.coalesced.framePending = false - wrapper := &w.decorations.Ops - off := op.Offset(w.lastFrame.off).Push(wrapper) - ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal)) - off.Pop() - w.lastFrame.deco.Add(wrapper) - if err := w.validateAndProcess(w.lastFrame.size, w.lastFrame.sync, wrapper, ack); err != nil { - w.destroyGPU() - w.gpuErr = err - w.driver.Perform(system.ActionClose) - return - } - w.updateState() - w.updateCursor() -} - -func (w *Window) updateState() { - for k := range w.semantic.ids { - delete(w.semantic.ids, k) - } - w.semantic.uptodate = false - q := &w.queue - switch q.TextInputState() { - case input.TextInputOpen: - w.driver.ShowTextInput(true) - case input.TextInputClose: - w.driver.ShowTextInput(false) - } - if hint, ok := q.TextInputHint(); ok { - w.driver.SetInputHint(hint) - } - if mime, txt, ok := q.WriteClipboard(); ok { - w.driver.WriteClipboard(mime, txt) - } - if q.ClipboardRequested() { - w.driver.ReadClipboard() - } - oldState := w.imeState - newState := oldState - newState.EditorState = q.EditorState() - if newState != oldState { - w.imeState = newState - w.driver.EditorStateChanged(oldState, newState) - } - if t, ok := q.WakeupTime(); ok { - w.setNextFrame(t) - } - w.updateAnimation() -} - -// Invalidate the window such that a [FrameEvent] will be generated immediately. -// If the window is inactive, an unspecified event is sent instead. -// -// Note that Invalidate is intended for externally triggered updates, such as a -// response from a network request. The [op.InvalidateCmd] command is more efficient -// for animation. -// -// Invalidate is safe for concurrent use. -func (w *Window) Invalidate() { - w.invMu.Lock() - defer w.invMu.Unlock() - if w.mayInvalidate { - w.mayInvalidate = false - w.driver.Invalidate() - } -} - -// Option applies the options to the window. The options are hints; the platform is -// free to ignore or adjust them. -func (w *Window) Option(opts ...Option) { - if len(opts) == 0 { - return - } - if w.driver == nil { - w.initialOpts = append(w.initialOpts, opts...) - return - } - w.Run(func() { - cnf := Config{Decorated: w.decorations.enabled} - for _, opt := range opts { - opt(w.metric, &cnf) - } - w.decorations.enabled = cnf.Decorated - decoHeight := w.decorations.height - if !w.decorations.enabled { - decoHeight = 0 - } - opts = append(opts, decoHeightOpt(decoHeight)) - w.driver.Configure(opts) - w.setNextFrame(time.Time{}) - w.updateAnimation() - }) -} - -// Run f in the same thread as the native window event loop, and wait for f to -// return or the window to close. If the window has not yet been created, -// Run calls f directly. -// -// Note that most programs should not call Run; configuring a Window with -// [CustomRenderer] is a notable exception. -func (w *Window) Run(f func()) { - if w.driver == nil { - f() - return - } - done := make(chan struct{}) - w.driver.Run(func() { - defer close(done) - f() - }) - <-done -} - -func (w *Window) updateAnimation() { - if w.driver == nil { - return - } - animate := false - if w.hasNextFrame { - if dt := time.Until(w.nextFrame); dt <= 0 { - animate = true - } else { - // Schedule redraw. - w.scheduleInvalidate(w.nextFrame) - } - } - if animate != w.animating { - w.animating = animate - w.driver.SetAnimating(animate) - } -} - -func (w *Window) scheduleInvalidate(t time.Time) { - if w.timer.quit == nil { - w.timer.quit = make(chan struct{}) - w.timer.update = make(chan time.Time) - go func() { - var timer *time.Timer - for { - var timeC <-chan time.Time - if timer != nil { - timeC = timer.C - } - select { - case <-w.timer.quit: - w.timer.quit <- struct{}{} - return - case t := <-w.timer.update: - if timer != nil { - timer.Stop() - } - timer = time.NewTimer(time.Until(t)) - case <-timeC: - w.Invalidate() - } - } - }() - } - w.timer.update <- t -} - -func (w *Window) setNextFrame(at time.Time) { - if !w.hasNextFrame || at.Before(w.nextFrame) { - w.hasNextFrame = true - w.nextFrame = at - } -} - -func (c *callbacks) SetDriver(d driver) { - if d == nil { - panic("nil driver") - } - c.w.invMu.Lock() - defer c.w.invMu.Unlock() - c.w.driver = d -} - -func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) { - c.w.processFrame(frame, ack) -} - -func (c *callbacks) ProcessEvent(e event.Event) bool { - return c.w.processEvent(e) -} - -// SemanticRoot returns the ID of the semantic root. -func (c *callbacks) SemanticRoot() input.SemanticID { - c.w.updateSemantics() - return c.w.semantic.root -} - -// LookupSemantic looks up a semantic node from an ID. The zero ID denotes the root. -func (c *callbacks) LookupSemantic(semID input.SemanticID) (input.SemanticNode, bool) { - c.w.updateSemantics() - n, found := c.w.semantic.ids[semID] - return n, found -} - -func (c *callbacks) AppendSemanticDiffs(diffs []input.SemanticID) []input.SemanticID { - c.w.updateSemantics() - if tree := c.w.semantic.prevTree; len(tree) > 0 { - c.w.collectSemanticDiffs(&diffs, c.w.semantic.prevTree[0]) - } - return diffs -} - -func (c *callbacks) SemanticAt(pos f32.Point) (input.SemanticID, bool) { - c.w.updateSemantics() - return c.w.queue.SemanticAt(pos) -} - -func (c *callbacks) EditorState() editorState { - return c.w.imeState -} - -func (c *callbacks) SetComposingRegion(r key.Range) { - c.w.imeState.compose = r -} - -func (c *callbacks) EditorInsert(text string) { - sel := c.w.imeState.Selection.Range - c.EditorReplace(sel, text) - start := min(sel.End, sel.Start) - sel.Start = start + utf8.RuneCountInString(text) - sel.End = sel.Start - c.SetEditorSelection(sel) -} - -func (c *callbacks) EditorReplace(r key.Range, text string) { - c.w.imeState.Replace(r, text) - c.w.driver.ProcessEvent(key.EditEvent{Range: r, Text: text}) - c.w.driver.ProcessEvent(key.SnippetEvent(c.w.imeState.Snippet.Range)) -} - -func (c *callbacks) SetEditorSelection(r key.Range) { - c.w.imeState.Selection.Range = r - c.w.driver.ProcessEvent(key.SelectionEvent(r)) -} - -func (c *callbacks) SetEditorSnippet(r key.Range) { - if sn := c.EditorState().Snippet.Range; sn == r { - // No need to expand. - return - } - c.w.driver.ProcessEvent(key.SnippetEvent(r)) -} - -func (w *Window) moveFocus(dir key.FocusDirection) { - w.queue.MoveFocus(dir) - if _, handled := w.queue.WakeupTime(); handled { - w.queue.RevealFocus(w.viewport) - } else { - var v image.Point - switch dir { - case key.FocusRight: - v = image.Pt(+1, 0) - case key.FocusLeft: - v = image.Pt(-1, 0) - case key.FocusDown: - v = image.Pt(0, +1) - case key.FocusUp: - v = image.Pt(0, -1) - default: - return - } - const scrollABit = unit.Dp(50) - dist := v.Mul(int(w.metric.Dp(scrollABit))) - w.queue.ScrollFocus(dist) - } -} - -func (c *callbacks) ClickFocus() { - c.w.queue.ClickFocus() - c.w.setNextFrame(time.Time{}) - c.w.updateAnimation() -} - -func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) { - return c.w.queue.ActionAt(p) -} - -func (w *Window) destroyGPU() { - if w.gpu != nil { - w.ctx.Lock() - w.gpu.Release() - w.ctx.Unlock() - w.gpu = nil - } - if w.ctx != nil { - w.ctx.Release() - w.ctx = nil - } -} - -// updateSemantics refreshes the semantics tree, the id to node map and the ids of -// updated nodes. -func (w *Window) updateSemantics() { - if w.semantic.uptodate { - return - } - w.semantic.uptodate = true - w.semantic.prevTree, w.semantic.tree = w.semantic.tree, w.semantic.prevTree - w.semantic.tree = w.queue.AppendSemantics(w.semantic.tree[:0]) - w.semantic.root = w.semantic.tree[0].ID - for _, n := range w.semantic.tree { - w.semantic.ids[n.ID] = n - } -} - -// collectSemanticDiffs traverses the previous semantic tree, noting changed nodes. -func (w *Window) collectSemanticDiffs(diffs *[]input.SemanticID, n input.SemanticNode) { - newNode, exists := w.semantic.ids[n.ID] - // Ignore deleted nodes, as their disappearance will be reported through an - // ancestor node. - if !exists { - return - } - diff := newNode.Desc != n.Desc || len(n.Children) != len(newNode.Children) - for i, ch := range n.Children { - if !diff { - newCh := newNode.Children[i] - diff = ch.ID != newCh.ID - } - w.collectSemanticDiffs(diffs, ch) - } - if diff { - *diffs = append(*diffs, n.ID) - } -} - -func (c *callbacks) Invalidate() { - c.w.setNextFrame(time.Time{}) - c.w.updateAnimation() - // Guarantee a wakeup, even when not animating. - c.w.processEvent(wakeupEvent{}) -} - -func (c *callbacks) nextEvent() (event.Event, bool) { - return c.w.nextEvent() -} - -func (w *Window) nextEvent() (event.Event, bool) { - s := &w.coalesced - defer func() { - // Every event counts as a wakeup. - s.wakeup = false - }() - switch { - case s.framePending: - // If the user didn't call FrameEvent.Event, process - // an empty frame. - w.processFrame(new(op.Ops), nil) - case s.view != nil: - e := *s.view - s.view = nil - return e, true - case s.destroy != nil: - e := *s.destroy - // Clear pending events after DestroyEvent is delivered. - *s = eventSummary{} - return e, true - case s.cfg != nil: - e := *s.cfg - s.cfg = nil - return e, true - case s.frame != nil: - e := *s.frame - s.frame = nil - s.framePending = true - return e.FrameEvent, true - case s.wakeup: - return wakeupEvent{}, true - } - w.invMu.Lock() - defer w.invMu.Unlock() - w.mayInvalidate = w.driver != nil - return nil, false -} - -func (w *Window) processEvent(e event.Event) bool { - switch e2 := e.(type) { - case wakeupEvent: - w.coalesced.wakeup = true - case frameEvent: - if e2.Size == (image.Point{}) { - panic(errors.New("internal error: zero-sized Draw")) - } - w.metric = e2.Metric - w.hasNextFrame = false - e2.Frame = w.driver.Frame - e2.Source = w.queue.Source() - // Prepare the decorations and update the frame insets. - viewport := image.Rectangle{ - Min: image.Point{ - X: e2.Metric.Dp(e2.Insets.Left), - Y: e2.Metric.Dp(e2.Insets.Top), - }, - Max: image.Point{ - X: e2.Size.X - e2.Metric.Dp(e2.Insets.Right), - Y: e2.Size.Y - e2.Metric.Dp(e2.Insets.Bottom), - }, - } - // Scroll to focus if viewport is shrinking in any dimension. - if old, new := w.viewport.Size(), viewport.Size(); new.X < old.X || new.Y < old.Y { - w.queue.RevealFocus(viewport) - } - w.viewport = viewport - wrapper := &w.decorations.Ops - wrapper.Reset() - m := op.Record(wrapper) - offset := w.decorate(e2.FrameEvent, wrapper) - w.lastFrame.deco = m.Stop() - w.lastFrame.size = e2.Size - w.lastFrame.sync = e2.Sync - w.lastFrame.off = offset - e2.Size = e2.Size.Sub(offset) - w.coalesced.frame = &e2 - case DestroyEvent: - if w.gpuErr != nil { - e2.Err = w.gpuErr - } - w.destroyGPU() - w.invMu.Lock() - w.mayInvalidate = false - w.driver = nil - w.invMu.Unlock() - if q := w.timer.quit; q != nil { - q <- struct{}{} - <-q - } - w.coalesced.destroy = &e2 - case ViewEvent: - if !e2.Valid() && w.gpu != nil { - w.ctx.Lock() - w.gpu.Release() - w.gpu = nil - w.ctx.Unlock() - } - w.coalesced.view = &e2 - case ConfigEvent: - w.decorations.Decorations.Maximized = e2.Config.Mode == Maximized - wasFocused := w.decorations.Config.Focused - w.decorations.Config = e2.Config - e2.Config = w.effectiveConfig() - w.coalesced.cfg = &e2 - if f := w.decorations.Config.Focused; f != wasFocused { - w.queue.Queue(key.FocusEvent{Focus: f}) - } - t, handled := w.queue.WakeupTime() - if handled { - w.setNextFrame(t) - w.updateAnimation() - } - return handled - case event.Event: - focusDir := key.FocusDirection(-1) - if e, ok := e2.(key.Event); ok && e.State == key.Press { - isMobile := runtime.GOOS == "ios" || runtime.GOOS == "android" - switch { - case e.Name == key.NameTab && e.Modifiers == 0: - focusDir = key.FocusForward - case e.Name == key.NameTab && e.Modifiers == key.ModShift: - focusDir = key.FocusBackward - case e.Name == key.NameUpArrow && e.Modifiers == 0 && isMobile: - focusDir = key.FocusUp - case e.Name == key.NameDownArrow && e.Modifiers == 0 && isMobile: - focusDir = key.FocusDown - case e.Name == key.NameLeftArrow && e.Modifiers == 0 && isMobile: - focusDir = key.FocusLeft - case e.Name == key.NameRightArrow && e.Modifiers == 0 && isMobile: - focusDir = key.FocusRight - } - } - e := e2 - if focusDir != -1 { - e = input.SystemEvent{Event: e} - } - w.queue.Queue(e) - t, handled := w.queue.WakeupTime() - if focusDir != -1 && !handled { - w.moveFocus(focusDir) - t, handled = w.queue.WakeupTime() - } - w.updateCursor() - if handled { - w.setNextFrame(t) - w.updateAnimation() - } - return handled - } - return true -} - -// Event blocks until an event is received from the window, such as -// [FrameEvent], or until [Invalidate] is called. The window is created -// and shown the first time Event is called. -func (w *Window) Event() event.Event { - if w.driver == nil { - w.init() - } - if w.driver == nil { - e, ok := w.nextEvent() - if !ok { - panic("window initialization failed without a DestroyEvent") - } - return e - } - return w.driver.Event() -} - -func (w *Window) init() { - debug.Parse() - // Measure decoration height. - deco := new(widget.Decorations) - theme := material.NewTheme() - theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular())) - decoStyle := material.Decorations(theme, deco, 0, "") - gtx := layout.Context{ - Ops: new(op.Ops), - // Measure in Dp. - Metric: unit.Metric{}, - } - // Allow plenty of space. - gtx.Constraints.Max.Y = 200 - dims := decoStyle.Layout(gtx) - decoHeight := unit.Dp(dims.Size.Y) - defaultOptions := []Option{ - Size(800, 600), - Title("Gio"), - Decorated(true), - decoHeightOpt(decoHeight), - } - options := append(defaultOptions, w.initialOpts...) - w.initialOpts = nil - var cnf Config - cnf.apply(unit.Metric{}, options) - - w.nocontext = cnf.CustomRenderer - w.decorations.Theme = theme - w.decorations.Decorations = deco - w.decorations.enabled = cnf.Decorated - w.decorations.height = decoHeight - w.imeState.compose = key.Range{Start: -1, End: -1} - w.semantic.ids = make(map[input.SemanticID]input.SemanticNode) - newWindow(&callbacks{w}, options) - for _, acts := range w.initialActions { - w.Perform(acts) - } - w.initialActions = nil -} - -func (w *Window) updateCursor() { - if c := w.queue.Cursor(); c != w.cursor { - w.cursor = c - w.driver.SetCursor(c) - } -} - -func (w *Window) fallbackDecorate() bool { - cnf := w.decorations.Config - return w.decorations.enabled && !cnf.Decorated && cnf.Mode != Fullscreen && !w.nocontext -} - -// decorate the window if enabled and returns the corresponding Insets. -func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point { - if !w.fallbackDecorate() { - return image.Pt(0, 0) - } - deco := w.decorations.Decorations - allActions := system.ActionMinimize | system.ActionMaximize | system.ActionUnmaximize | - system.ActionClose | system.ActionMove - style := material.Decorations(w.decorations.Theme, deco, allActions, w.decorations.Config.Title) - // Update the decorations based on the current window mode. - var actions system.Action - switch m := w.decorations.Config.Mode; m { - case Windowed: - actions |= system.ActionUnmaximize - case Minimized: - actions |= system.ActionMinimize - case Maximized: - actions |= system.ActionMaximize - case Fullscreen: - actions |= system.ActionFullscreen - default: - panic(fmt.Errorf("unknown WindowMode %v", m)) - } - gtx := layout.Context{ - Ops: o, - Now: e.Now, - Source: e.Source, - Metric: e.Metric, - Constraints: layout.Exact(e.Size), - } - // Update the window based on the actions on the decorations. - opts, acts := splitActions(deco.Update(gtx)) - if len(opts) > 0 { - w.driver.Configure(opts) - } - if acts != 0 { - w.driver.Perform(acts) - } - style.Layout(gtx) - // Offset to place the frame content below the decorations. - decoHeight := gtx.Dp(w.decorations.Config.decoHeight) - if w.decorations.currentHeight != decoHeight { - w.decorations.currentHeight = decoHeight - w.coalesced.cfg = &ConfigEvent{Config: w.effectiveConfig()} - } - return image.Pt(0, decoHeight) -} - -func (w *Window) effectiveConfig() Config { - cnf := w.decorations.Config - cnf.Size.Y -= w.decorations.currentHeight - cnf.Decorated = w.decorations.enabled || cnf.Decorated - return cnf -} - -// splitActions splits options from actions and return them and the remaining -// actions. -func splitActions(actions system.Action) ([]Option, system.Action) { - var opts []Option - walkActions(actions, func(action system.Action) { - switch action { - case system.ActionMinimize: - opts = append(opts, Minimized.Option()) - case system.ActionMaximize: - opts = append(opts, Maximized.Option()) - case system.ActionUnmaximize: - opts = append(opts, Windowed.Option()) - case system.ActionFullscreen: - opts = append(opts, Fullscreen.Option()) - default: - return - } - actions &^= action - }) - return opts, actions -} - -// Perform the actions on the window. -func (w *Window) Perform(actions system.Action) { - opts, acts := splitActions(actions) - w.Option(opts...) - if acts == 0 { - return - } - if w.driver == nil { - w.initialActions = append(w.initialActions, acts) - return - } - w.Run(func() { - w.driver.Perform(actions) - }) -} - -// Title sets the title of the window. -func Title(t string) Option { - return func(_ unit.Metric, cnf *Config) { - cnf.Title = t - } -} - -// Size sets the size of the window. The mode will be changed to Windowed. -func Size(w, h unit.Dp) Option { - if w <= 0 { - panic("width must be larger than or equal to 0") - } - if h <= 0 { - panic("height must be larger than or equal to 0") - } - return func(m unit.Metric, cnf *Config) { - cnf.Mode = Windowed - cnf.Size = image.Point{ - X: m.Dp(w), - Y: m.Dp(h), - } - } -} - -// MaxSize sets the maximum size of the window. -func MaxSize(w, h unit.Dp) Option { - if w <= 0 { - panic("width must be larger than or equal to 0") - } - if h <= 0 { - panic("height must be larger than or equal to 0") - } - return func(m unit.Metric, cnf *Config) { - cnf.MaxSize = image.Point{ - X: m.Dp(w), - Y: m.Dp(h), - } - } -} - -// MinSize sets the minimum size of the window. -func MinSize(w, h unit.Dp) Option { - if w <= 0 { - panic("width must be larger than or equal to 0") - } - if h <= 0 { - panic("height must be larger than or equal to 0") - } - return func(m unit.Metric, cnf *Config) { - cnf.MinSize = image.Point{ - X: m.Dp(w), - Y: m.Dp(h), - } - } -} - -// StatusColor sets the color of the Android status bar. -func StatusColor(color color.NRGBA) Option { - return func(_ unit.Metric, cnf *Config) { - cnf.StatusColor = color - } -} - -// NavigationColor sets the color of the navigation bar on Android, or the address bar in browsers. -func NavigationColor(color color.NRGBA) Option { - return func(_ unit.Metric, cnf *Config) { - cnf.NavigationColor = color - } -} - -// CustomRenderer controls whether the window contents is -// rendered by the client. If true, no GPU context is created. -// -// Caller must assume responsibility for rendering which includes -// initializing the render backend, swapping the framebuffer and -// handling frame pacing. -func CustomRenderer(custom bool) Option { - return func(_ unit.Metric, cnf *Config) { - cnf.CustomRenderer = custom - } -} - -// Decorated controls whether Gio and/or the platform are responsible -// for drawing window decorations. Providing false indicates that -// the application will either be undecorated or will draw its own decorations. -func Decorated(enabled bool) Option { - return func(_ unit.Metric, cnf *Config) { - cnf.Decorated = enabled - } -} - -// flushEvent is sent to detect when the user program -// has completed processing of all prior events. Its an -// [io/event.Event] but only for internal use. -type flushEvent struct{} - -func (t flushEvent) ImplementsEvent() {} - -// theFlushEvent avoids allocating garbage when sending -// flushEvents. -var theFlushEvent flushEvent diff --git a/gio/f32/affine.go b/gio/f32/affine.go deleted file mode 100644 index f1aa90e..0000000 --- a/gio/f32/affine.go +++ /dev/null @@ -1,181 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package f32 - -import ( - "math" - "strconv" -) - -// Affine2D represents an affine 2D transformation. The zero value of Affine2D -// represents the identity transform. -type Affine2D struct { - // in order to make the zero value of Affine2D represent the identity - // transform we store it with the identity matrix subtracted, that is - // if the actual transformation matrix is: - // [sx, hx, ox] - // [hy, sy, oy] - // [ 0, 0, 1] - // we store a = sx-1 and e = sy-1 - a, b, c float32 - d, e, f float32 -} - -// NewAffine2D creates a new Affine2D transform from the matrix elements -// in row major order. The rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. -func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D { - return Affine2D{ - a: sx - 1, b: hx, c: ox, - d: hy, e: sy - 1, f: oy, - } -} - -// AffineId returns an identity transformation matrix that represents no transformation -// when applied. -func AffineId() Affine2D { - return NewAffine2D( - 1, 0, 0, - 0, 1, 0, - ) -} - -// Offset the transformation. -func (a Affine2D) Offset(offset Point) Affine2D { - return Affine2D{ - a.a, a.b, a.c + offset.X, - a.d, a.e, a.f + offset.Y, - } -} - -// Scale the transformation around the given origin. -func (a Affine2D) Scale(origin, factor Point) Affine2D { - if origin == (Point{}) { - return a.scale(factor) - } - a = a.Offset(origin.Mul(-1)) - a = a.scale(factor) - return a.Offset(origin) -} - -// Rotate the transformation by the given angle (in radians) counter clockwise around the given origin. -func (a Affine2D) Rotate(origin Point, radians float32) Affine2D { - if origin == (Point{}) { - return a.rotate(radians) - } - a = a.Offset(origin.Mul(-1)) - a = a.rotate(radians) - return a.Offset(origin) -} - -// Shear the transformation by the given angle (in radians) around the given origin. -func (a Affine2D) Shear(origin Point, radiansX, radiansY float32) Affine2D { - if origin == (Point{}) { - return a.shear(radiansX, radiansY) - } - a = a.Offset(origin.Mul(-1)) - a = a.shear(radiansX, radiansY) - return a.Offset(origin) -} - -// Mul returns A*B. -func (A Affine2D) Mul(B Affine2D) (r Affine2D) { - r.a = (A.a+1)*(B.a+1) + A.b*B.d - 1 - r.b = (A.a+1)*B.b + A.b*(B.e+1) - r.c = (A.a+1)*B.c + A.b*B.f + A.c - r.d = A.d*(B.a+1) + (A.e+1)*B.d - r.e = A.d*B.b + (A.e+1)*(B.e+1) - 1 - r.f = A.d*B.c + (A.e+1)*B.f + A.f - return r -} - -// Invert the transformation. Note that if the matrix is close to singular -// numerical errors may become large or infinity. -func (a Affine2D) Invert() Affine2D { - if a.a == 0 && a.b == 0 && a.d == 0 && a.e == 0 { - return Affine2D{a: 0, b: 0, c: -a.c, d: 0, e: 0, f: -a.f} - } - a.a += 1 - a.e += 1 - det := a.a*a.e - a.b*a.d - a.a, a.e = a.e/det, a.a/det - a.b, a.d = -a.b/det, -a.d/det - temp := a.c - a.c = -a.a*a.c - a.b*a.f - a.f = -a.d*temp - a.e*a.f - a.a -= 1 - a.e -= 1 - return a -} - -// Transform p by returning a*p. -func (a Affine2D) Transform(p Point) Point { - return Point{ - X: p.X*(a.a+1) + p.Y*a.b + a.c, - Y: p.X*a.d + p.Y*(a.e+1) + a.f, - } -} - -// Elems returns the matrix elements of the transform in row-major order. The -// rows are: [sx, hx, ox], [hy, sy, oy], [0, 0, 1]. -func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) { - return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f -} - -// Split a transform into two parts, one which is pure offset and the -// other representing the scaling, shearing and rotation part. -func (a Affine2D) Split() (srs Affine2D, offset Point) { - return Affine2D{ - a: a.a, b: a.b, c: 0, - d: a.d, e: a.e, f: 0, - }, Point{X: a.c, Y: a.f} -} - -func (a Affine2D) scale(factor Point) Affine2D { - return Affine2D{ - (a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X, - a.d * factor.Y, (a.e+1)*factor.Y - 1, a.f * factor.Y, - } -} - -func (a Affine2D) rotate(radians float32) Affine2D { - sin, cos := math.Sincos(float64(radians)) - s, c := float32(sin), float32(cos) - return Affine2D{ - (a.a+1)*c - a.d*s - 1, a.b*c - (a.e+1)*s, a.c*c - a.f*s, - (a.a+1)*s + a.d*c, a.b*s + (a.e+1)*c - 1, a.c*s + a.f*c, - } -} - -func (a Affine2D) shear(radiansX, radiansY float32) Affine2D { - tx := float32(math.Tan(float64(radiansX))) - ty := float32(math.Tan(float64(radiansY))) - return Affine2D{ - (a.a + 1) + a.d*tx - 1, a.b + (a.e+1)*tx, a.c + a.f*tx, - (a.a+1)*ty + a.d, a.b*ty + (a.e + 1) - 1, a.f*ty + a.f, - } -} - -func (a Affine2D) String() string { - sx, hx, ox, hy, sy, oy := a.Elems() - - // precision 6, one period, negative sign and space per number - const prec = 6 - const charsPerFloat = prec + 2 + 1 - s := make([]byte, 0, 6*charsPerFloat+6) - - s = append(s, '[', '[') - s = strconv.AppendFloat(s, float64(sx), 'g', prec, 32) - s = append(s, ' ') - s = strconv.AppendFloat(s, float64(hx), 'g', prec, 32) - s = append(s, ' ') - s = strconv.AppendFloat(s, float64(ox), 'g', prec, 32) - s = append(s, ']', ' ', '[') - s = strconv.AppendFloat(s, float64(hy), 'g', prec, 32) - s = append(s, ' ') - s = strconv.AppendFloat(s, float64(sy), 'g', prec, 32) - s = append(s, ' ') - s = strconv.AppendFloat(s, float64(oy), 'g', prec, 32) - s = append(s, ']', ']') - - return string(s) -} diff --git a/gio/f32/affine_test.go b/gio/f32/affine_test.go deleted file mode 100644 index 80baad5..0000000 --- a/gio/f32/affine_test.go +++ /dev/null @@ -1,385 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package f32 - -import ( - "math" - "testing" -) - -func eq(p1, p2 Point) bool { - tol := 1e-5 - dx, dy := p2.X-p1.X, p2.Y-p1.Y - return math.Abs(math.Sqrt(float64(dx*dx+dy*dy))) < tol -} - -func eqaff(x, y Affine2D) bool { - tol := 1e-5 - return math.Abs(float64(x.a-y.a)) < tol && - math.Abs(float64(x.b-y.b)) < tol && - math.Abs(float64(x.c-y.c)) < tol && - math.Abs(float64(x.d-y.d)) < tol && - math.Abs(float64(x.e-y.e)) < tol && - math.Abs(float64(x.f-y.f)) < tol -} - -func TestTransformOffset(t *testing.T) { - p := Point{X: 1, Y: 2} - o := Point{X: 2, Y: -3} - - r := AffineId().Offset(o).Transform(p) - if !eq(r, Pt(3, -1)) { - t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r) - } - i := AffineId().Offset(o).Invert().Transform(r) - if !eq(i, p) { - t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p) - } -} - -func TestString(t *testing.T) { - tests := []struct { - in Affine2D - exp string - }{ - { - in: NewAffine2D(9, 11, 13, 17, 19, 23), - exp: "[[9 11 13] [17 19 23]]", - }, { - in: NewAffine2D(29, 31, 37, 43, 47, 53), - exp: "[[29 31 37] [43 47 53]]", - }, { - in: NewAffine2D(29.142342, 31.4123412, 37.53152, 43.51324213, 47.123412, 53.14312342), - exp: "[[29.1423 31.4123 37.5315] [43.5132 47.1234 53.1431]]", - }, { - in: AffineId(), - exp: "[[1 0 0] [0 1 0]]", - }, - } - for _, test := range tests { - if test.in.String() != test.exp { - t.Errorf("string mismatch: have %q, want %q", test.in.String(), test.exp) - } - } -} - -func TestTransformScale(t *testing.T) { - p := Point{X: 1, Y: 2} - s := Point{X: -1, Y: 2} - - r := AffineId().Scale(Point{}, s).Transform(p) - if !eq(r, Pt(-1, 4)) { - t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r) - } - i := AffineId().Scale(Point{}, s).Invert().Transform(r) - if !eq(i, p) { - t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p) - } -} - -func TestTransformRotate(t *testing.T) { - p := Point{X: 1, Y: 0} - a := float32(math.Pi / 2) - - r := AffineId().Rotate(Point{}, a).Transform(p) - if !eq(r, Pt(0, 1)) { - t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r) - } - i := AffineId().Rotate(Point{}, a).Invert().Transform(r) - if !eq(i, p) { - t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p) - } -} - -func TestTransformShear(t *testing.T) { - p := Point{X: 1, Y: 1} - - r := AffineId().Shear(Point{}, math.Pi/4, 0).Transform(p) - if !eq(r, Pt(2, 1)) { - t.Errorf("shear transformation mismatch: have %v, want {2 1}", r) - } - i := AffineId().Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) - if !eq(i, p) { - t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p) - } -} - -func TestTransformMultiply(t *testing.T) { - p := Point{X: 1, Y: 2} - o := Point{X: 2, Y: -3} - s := Point{X: -1, Y: 2} - a := float32(-math.Pi / 2) - - r := AffineId().Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Transform(p) - if !eq(r, Pt(1, 3)) { - t.Errorf("complex transformation mismatch: have %v, want {1 3}", r) - } - i := AffineId().Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Invert().Transform(r) - if !eq(i, p) { - t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p) - } -} - -func TestPrimes(t *testing.T) { - xa := NewAffine2D(9, 11, 13, 17, 19, 23) - xb := NewAffine2D(29, 31, 37, 43, 47, 53) - - pa := Point{X: 2, Y: 3} - pb := Point{X: 5, Y: 7} - - for _, test := range []struct { - x Affine2D - p Point - exp Point - }{ - {x: xa, p: pa, exp: Pt(64, 114)}, - {x: xa, p: pb, exp: Pt(135, 241)}, - {x: xb, p: pa, exp: Pt(188, 280)}, - {x: xb, p: pb, exp: Pt(399, 597)}, - } { - got := test.x.Transform(test.p) - if !eq(got, test.exp) { - t.Errorf("%v.Transform(%v): have %v, want %v", test.x, test.p, got, test.exp) - } - } - - for _, test := range []struct { - x Affine2D - exp Affine2D - }{ - {x: xa, exp: NewAffine2D(-1.1875, 0.6875, -0.375, 1.0625, -0.5625, -0.875)}, - {x: xb, exp: NewAffine2D(1.5666667, -1.0333333, -3.2000008, -1.4333333, 1-0.03333336, 1.7999992)}, - } { - got := test.x.Invert() - if !eqaff(got, test.exp) { - t.Errorf("%v.Invert(): have %v, want %v", test.x, got, test.exp) - } - } - - got := xa.Mul(xb) - exp := NewAffine2D(734, 796, 929, 1310, 1420, 1659) - if !eqaff(got, exp) { - t.Errorf("%v.Mul(%v): have %v, want %v", xa, xb, got, exp) - } -} - -func TestTransformScaleAround(t *testing.T) { - p := Pt(-1, -1) - target := Pt(-6, -13) - pt := AffineId().Scale(Pt(4, 5), Pt(2, 3)).Transform(p) - if !eq(pt, target) { - t.Log(pt, "!=", target) - t.Error("Scale not as expected") - } -} - -func TestTransformRotateAround(t *testing.T) { - p := Pt(-1, -1) - pt := AffineId().Rotate(Pt(1, 1), -math.Pi/2).Transform(p) - target := Pt(-1, 3) - if !eq(pt, target) { - t.Log(pt, "!=", target) - t.Error("Rotate not as expected") - } -} - -func TestMulOrder(t *testing.T) { - A := AffineId().Offset(Pt(100, 100)) - B := AffineId().Scale(Point{}, Pt(2, 2)) - _ = A - _ = B - - T1 := AffineId().Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2)) - T2 := B.Mul(A) - - if T1 != T2 { - t.Log(T1) - t.Log(T2) - t.Error("multiplication / transform order not as expected") - } -} - -func BenchmarkTransformOffset(b *testing.B) { - p := Point{X: 1, Y: 2} - o := Point{X: 0.5, Y: 0.5} - aff := AffineId().Offset(o) - - for b.Loop() { - p = aff.Transform(p) - } - _ = p -} - -func BenchmarkTransformScale(b *testing.B) { - p := Point{X: 1, Y: 2} - s := Point{X: 0.5, Y: 0.5} - aff := AffineId().Scale(Point{}, s) - for b.Loop() { - p = aff.Transform(p) - } - _ = p -} - -func BenchmarkTransformRotate(b *testing.B) { - p := Point{X: 1, Y: 2} - a := float32(math.Pi / 2) - aff := AffineId().Rotate(Point{}, a) - for b.Loop() { - p = aff.Transform(p) - } - _ = p -} - -func BenchmarkTransformTranslateMultiply(b *testing.B) { - a := AffineId().Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) - t := AffineId().Offset(Point{X: 0.5, Y: 0.5}) - - for b.Loop() { - a = a.Mul(t) - } -} - -func BenchmarkTransformScaleMultiply(b *testing.B) { - a := AffineId().Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) - t := AffineId().Offset(Point{X: 0.5, Y: 0.5}).Scale(Point{}, Point{X: 0.4, Y: -0.5}) - - for b.Loop() { - a = a.Mul(t) - } -} - -func BenchmarkTransformMultiply(b *testing.B) { - a := AffineId().Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) - t := AffineId().Offset(Point{X: 0.5, Y: 0.5}).Rotate(Point{}, math.Pi/7) - - for b.Loop() { - a = a.Mul(t) - } -} - -func TestNewAffine2D(t *testing.T) { - tests := []struct { - sx, hx, ox, hy, sy, oy float32 - expected Affine2D - }{ - {1, 0, 0, 0, 1, 0, AffineId()}, - {2, 0, 5, 0, 3, 7, Affine2D{a: 1, b: 0, c: 5, d: 0, e: 2, f: 7}}, - {-1, 2, 3, 4, -5, 6, Affine2D{a: -2, b: 2, c: 3, d: 4, e: -6, f: 6}}, - } - - for i, test := range tests { - got := NewAffine2D(test.sx, test.hx, test.ox, test.hy, test.sy, test.oy) - if !eqaff(got, test.expected) { - t.Errorf( - "Test %d: NewAffine2D(%v, %v, %v, %v, %v, %v) = %v, want %v", - i, test.sx, test.hx, test.ox, test.hy, test.sy, test.oy, got, test.expected, - ) - } - } -} - -func TestAffineId(t *testing.T) { - id := AffineId() - - testPoints := []Point{ - {0, 0}, - {1, 0}, - {0, 1}, - {-1, -1}, - {10, 20}, - } - - for _, p := range testPoints { - transformed := id.Transform(p) - if !eq(transformed, p) { - t.Errorf("Identity transform changed point: %v -> %v", p, transformed) - } - } -} - -func TestElems(t *testing.T) { - tests := []struct { - aff Affine2D - sx, hx, ox, hy, sy, oy float32 - }{ - {AffineId(), 1, 0, 0, 0, 1, 0}, - {Affine2D{a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}, 2, 2, 3, 4, 6, 6}, - {NewAffine2D(7, 8, 9, 10, 11, 12), 7, 8, 9, 10, 11, 12}, - } - - for i, test := range tests { - sx, hx, ox, hy, sy, oy := test.aff.Elems() - if sx != test.sx || hx != test.hx || ox != test.ox || - hy != test.hy || sy != test.sy || oy != test.oy { - t.Errorf( - "Test %d: %v.Elems() = (%v, %v, %v, %v, %v, %v), want (%v, %v, %v, %v, %v, %v)", - i, test.aff, sx, hx, ox, hy, sy, oy, test.sx, test.hx, test.ox, test.hy, test.sy, test.oy, - ) - } - } -} - -func TestSplit(t *testing.T) { - tests := []struct { - aff Affine2D - expectedSRS Affine2D - expectedOffset Point - }{ - { - AffineId(), - AffineId(), - Point{0, 0}, - }, - { - Affine2D{a: 1, b: 2, c: 3, d: 4, e: 5, f: 6}, - Affine2D{a: 1, b: 2, c: 0, d: 4, e: 5, f: 0}, - Point{3, 6}, - }, - { - NewAffine2D(2, 0, 10, 0, 3, 20), - NewAffine2D(2, 0, 0, 0, 3, 0), - Point{10, 20}, - }, - } - - for i, test := range tests { - srs, offset := test.aff.Split() - if !eqaff(srs, test.expectedSRS) || !eq(offset, test.expectedOffset) { - t.Errorf( - "Test %d: %v.Split() = (%v, %v), want (%v, %v)", - i, test.aff, srs, offset, test.expectedSRS, test.expectedOffset, - ) - } - } -} - -func TestShear(t *testing.T) { - p := Pt(2, 3) - origin := Pt(1, 1) - - shearX := AffineId().Shear(origin, math.Pi/4, 0) - resultX := shearX.Transform(p) - expectedX := Pt(4, 3) - - if !eq(resultX, expectedX) { - t.Errorf("Shear around origin in X: got %v, want %v", resultX, expectedX) - } - - inverseX := shearX.Invert().Transform(resultX) - if !eq(inverseX, p) { - t.Errorf("Inverse shear X: got %v, want %v", inverseX, p) - } - - shearY := AffineId().Shear(origin, 0, math.Pi/4) - resultY := shearY.Transform(p) - expectedY := Pt(2, 4) - - if !eq(resultY, expectedY) { - t.Errorf("Shear around origin in Y: got %v, want %v", resultY, expectedY) - } - - inverseY := shearY.Invert().Transform(resultY) - if !eq(inverseY, p) { - t.Errorf("Inverse shear Y: got %v, want %v", inverseY, p) - } -} diff --git a/gio/f32/f32.go b/gio/f32/f32.go deleted file mode 100644 index 1938ffb..0000000 --- a/gio/f32/f32.go +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package f32 is a float32 implementation of package image's -Point and affine transformations. - -The coordinate space has the origin in the top left -corner with the axes extending right and down. -*/ -package f32 - -import ( - "image" - "math" - "strconv" -) - -// A Point is a two dimensional point. -type Point struct { - X, Y float32 -} - -// String return a string representation of p. -func (p Point) String() string { - return "(" + strconv.FormatFloat(float64(p.X), 'f', -1, 32) + - "," + strconv.FormatFloat(float64(p.Y), 'f', -1, 32) + ")" -} - -// Pt is shorthand for Point{X: x, Y: y}. -func Pt(x, y float32) Point { - return Point{X: x, Y: y} -} - -// Add return the point p+p2. -func (p Point) Add(p2 Point) Point { - return Point{X: p.X + p2.X, Y: p.Y + p2.Y} -} - -// Sub returns the vector p-p2. -func (p Point) Sub(p2 Point) Point { - return Point{X: p.X - p2.X, Y: p.Y - p2.Y} -} - -// Mul returns p scaled by s. -func (p Point) Mul(s float32) Point { - return Point{X: p.X * s, Y: p.Y * s} -} - -// Div returns the vector p/s. -func (p Point) Div(s float32) Point { - return Point{X: p.X / s, Y: p.Y / s} -} - -// Round returns the integer point closest to p. -func (p Point) Round() image.Point { - return image.Point{ - X: int(math.Round(float64(p.X))), - Y: int(math.Round(float64(p.Y))), - } -} diff --git a/gio/flake.lock b/gio/flake.lock deleted file mode 100644 index c27e97a..0000000 --- a/gio/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "nixpkgs": { - "locked": { - "lastModified": 1747953325, - "narHash": "sha256-y2ZtlIlNTuVJUZCqzZAhIw5rrKP4DOSklev6c8PyCkQ=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "55d1f923c480dadce40f5231feb472e81b0bab48", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-25.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "nixpkgs": "nixpkgs", - "utils": "utils" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/gio/flake.nix b/gio/flake.nix deleted file mode 100644 index a7dee57..0000000 --- a/gio/flake.nix +++ /dev/null @@ -1,54 +0,0 @@ -# SPDX-License-Identifier: Unlicense OR MIT -{ - description = "Gio build environment"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; - utils.url = "github:numtide/flake-utils"; - }; - - outputs = { self, nixpkgs, utils }: - utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { - inherit system; - - # allow unfree Android packages. - config.allowUnfree = true; - # accept the Android SDK license. - config.android_sdk.accept_license = true; - }; - in { - devShells = let - android-sdk = let - androidComposition = pkgs.androidenv.composeAndroidPackages { - platformVersions = [ "latest" ]; - abiVersions = [ "armeabi-v7a" "arm64-v8a" ]; - # Omit the deprecated tools package. - toolsVersion = null; - includeNDK = true; - }; - in androidComposition.androidsdk; - in { - default = with pkgs; - mkShell (rec { - ANDROID_HOME = "${android-sdk}/libexec/android-sdk"; - packages = [ android-sdk jdk clang ] - ++ (if stdenv.isLinux then [ - vulkan-headers - libxkbcommon - wayland - xorg.libX11 - xorg.libXcursor - xorg.libXfixes - libGL - pkg-config - ] else - [ ]); - } // (if stdenv.isLinux then { - LD_LIBRARY_PATH = "${vulkan-loader}/lib"; - } else - { })); - }; - }); -} diff --git a/gio/font/font.go b/gio/font/font.go deleted file mode 100644 index d998cf8..0000000 --- a/gio/font/font.go +++ /dev/null @@ -1,126 +0,0 @@ -/* -Package font provides type describing font faces attributes. -*/ -package font - -import ( - "github.com/go-text/typesetting/font" -) - -// A FontFace is a Font and a matching Face. -type FontFace struct { - Font Font - Face Face -} - -// Style is the font style. -type Style int - -// Weight is a font weight, in CSS units subtracted 400 so the zero value -// is normal text weight. -type Weight int - -// Font specify a particular typeface variant, style and weight. -type Font struct { - // Typeface specifies the name(s) of the the font faces to try. See [Typeface] - // for details. - Typeface Typeface - // Style specifies the kind of text style. - Style Style - // Weight is the text weight. - Weight Weight -} - -// Face is an opaque handle to a typeface. The concrete implementation depends -// upon the kind of font and shaper in use. -type Face interface { - Face() *font.Face -} - -// Typeface identifies a list of font families to attempt to use for displaying -// a string. The syntax is a comma-delimited list of family names. In order to -// allow for the remote possibility of needing to express a font family name -// containing a comma, name entries may be quoted using either single or double -// quotes. Within quotes, a literal quotation mark can be expressed by escaping -// it with `\`. A literal backslash may be expressed by escaping it with another -// `\`. -// -// Here's an example Typeface: -// -// Times New Roman, Georgia, serif -// -// This is equivalent to the above: -// -// "Times New Roman", 'Georgia', serif -// -// Here are some valid uses of escape sequences: -// -// "Contains a literal \" doublequote", 'Literal \' Singlequote', "\\ Literal backslash", '\\ another' -// -// This syntax has the happy side effect that most CSS "font-family" rules are -// valid Typefaces (without the trailing semicolon). -// -// Generic CSS font families are supported, and are automatically expanded to lists -// of known font families with a matching style. The supported generic families are: -// -// - fantasy -// - math -// - emoji -// - serif -// - sans-serif -// - cursive -// - monospace -type Typeface string - -const ( - Regular Style = iota - Italic -) - -const ( - Thin Weight = -300 - ExtraLight Weight = -200 - Light Weight = -100 - Normal Weight = 0 - Medium Weight = 100 - SemiBold Weight = 200 - Bold Weight = 300 - ExtraBold Weight = 400 - Black Weight = 500 -) - -func (s Style) String() string { - switch s { - case Regular: - return "Regular" - case Italic: - return "Italic" - default: - panic("invalid Style") - } -} - -func (w Weight) String() string { - switch w { - case Thin: - return "Thin" - case ExtraLight: - return "ExtraLight" - case Light: - return "Light" - case Normal: - return "Normal" - case Medium: - return "Medium" - case SemiBold: - return "SemiBold" - case Bold: - return "Bold" - case ExtraBold: - return "ExtraBold" - case Black: - return "Black" - default: - panic("invalid Weight") - } -} diff --git a/gio/font/gofont/gofont.go b/gio/font/gofont/gofont.go deleted file mode 100644 index 1954745..0000000 --- a/gio/font/gofont/gofont.go +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package gofont exports the Go fonts as a text.Collection. -// -// See https://blog.golang.org/go-fonts for a description of the -// fonts, and the golang.org/x/image/font/gofont packages for the -// font data. -package gofont - -import ( - "fmt" - "sync" - - "golang.org/x/image/font/gofont/gobold" - "golang.org/x/image/font/gofont/gobolditalic" - "golang.org/x/image/font/gofont/goitalic" - "golang.org/x/image/font/gofont/gomedium" - "golang.org/x/image/font/gofont/gomediumitalic" - "golang.org/x/image/font/gofont/gomono" - "golang.org/x/image/font/gofont/gomonobold" - "golang.org/x/image/font/gofont/gomonobolditalic" - "golang.org/x/image/font/gofont/gomonoitalic" - "golang.org/x/image/font/gofont/goregular" - "golang.org/x/image/font/gofont/gosmallcaps" - "golang.org/x/image/font/gofont/gosmallcapsitalic" - - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/font/opentype" -) - -var ( - regOnce sync.Once - reg []font.FontFace - once sync.Once - collection []font.FontFace -) - -func loadRegular() { - regOnce.Do(func() { - faces, err := opentype.ParseCollection(goregular.TTF) - if err != nil { - panic(fmt.Errorf("failed to parse font: %v", err)) - } - reg = faces - collection = append(collection, reg[0]) - }) -} - -// Regular returns a collection of only the Go regular font face. -func Regular() []font.FontFace { - loadRegular() - return reg -} - -// Regular returns a collection of all available Go font faces. -func Collection() []font.FontFace { - loadRegular() - once.Do(func() { - register(goitalic.TTF) - register(gobold.TTF) - register(gobolditalic.TTF) - register(gomedium.TTF) - register(gomediumitalic.TTF) - register(gomono.TTF) - register(gomonobold.TTF) - register(gomonobolditalic.TTF) - register(gomonoitalic.TTF) - register(gosmallcaps.TTF) - register(gosmallcapsitalic.TTF) - // Ensure that any outside appends will not reuse the backing store. - n := len(collection) - collection = collection[:n:n] - }) - return collection -} - -func register(ttf []byte) { - faces, err := opentype.ParseCollection(ttf) - if err != nil { - panic(fmt.Errorf("failed to parse font: %v", err)) - } - collection = append(collection, faces[0]) -} diff --git a/gio/font/opentype/opentype.go b/gio/font/opentype/opentype.go deleted file mode 100644 index 4ea56de..0000000 --- a/gio/font/opentype/opentype.go +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package opentype implements text layout and shaping for OpenType -// files. -// -// NOTE: the OpenType specification allows for fonts to include bitmap images -// in a variety of formats. In the interest of small binary sizes, the opentype -// package only automatically imports the PNG image decoder. If you have a font -// with glyphs in JPEG or TIFF formats, register those decoders with the image -// package in order to ensure those glyphs are visible in text. -package opentype - -import ( - "bytes" - "fmt" - _ "image/png" - - giofont "github.com/p9c/p9/pkg/gel/gio/font" - fontapi "github.com/go-text/typesetting/font" - "github.com/go-text/typesetting/font/opentype" -) - -// Face is a thread-safe representation of a loaded font. For efficiency, applications -// should construct a face for any given font file once, reusing it across different -// text shapers. -type Face struct { - face *fontapi.Font - font giofont.Font -} - -// Parse constructs a Face from source bytes. -func Parse(src []byte) (Face, error) { - ld, err := opentype.NewLoader(bytes.NewReader(src)) - if err != nil { - return Face{}, err - } - font, md, err := parseLoader(ld) - if err != nil { - return Face{}, fmt.Errorf("failed parsing truetype font: %w", err) - } - return Face{ - face: font, - font: md, - }, nil -} - -// ParseCollection parse an Opentype font file, with support for collections. -// Single font files are supported, returning a slice with length 1. -// The returned fonts are automatically wrapped in a text.FontFace with -// inferred font font. -// BUG(whereswaldon): the only Variant that can be detected automatically is -// "Mono". -func ParseCollection(src []byte) ([]giofont.FontFace, error) { - lds, err := opentype.NewLoaders(bytes.NewReader(src)) - if err != nil { - return nil, err - } - out := make([]giofont.FontFace, len(lds)) - for i, ld := range lds { - face, md, err := parseLoader(ld) - if err != nil { - return nil, fmt.Errorf("reading font %d of collection: %s", i, err) - } - ff := Face{ - face: face, - font: md, - } - out[i] = giofont.FontFace{ - Face: ff, - Font: ff.Font(), - } - } - - return out, nil -} - -func DescriptionToFont(md fontapi.Description) giofont.Font { - return giofont.Font{ - Typeface: giofont.Typeface(md.Family), - Style: gioStyle(md.Aspect.Style), - Weight: gioWeight(md.Aspect.Weight), - } -} - -func FontToDescription(font giofont.Font) fontapi.Description { - return fontapi.Description{ - Family: string(font.Typeface), - Aspect: fontapi.Aspect{ - Style: mdStyle(font.Style), - Weight: mdWeight(font.Weight), - }, - } -} - -// parseLoader parses the contents of the loader into a face and its font. -func parseLoader(ld *opentype.Loader) (*fontapi.Font, giofont.Font, error) { - ft, err := fontapi.NewFont(ld) - if err != nil { - return nil, giofont.Font{}, err - } - data := DescriptionToFont(ft.Describe()) - return ft, data, nil -} - -// Face returns a thread-unsafe wrapper for this Face suitable for use by a single shaper. -// Face many be invoked any number of times and is safe so long as each return value is -// only used by one goroutine. -func (f Face) Face() *fontapi.Face { - return &fontapi.Face{Font: f.face} -} - -// FontFace returns a text.Font with populated font metadata for the -// font. -// BUG(whereswaldon): the only Variant that can be detected automatically is -// "Mono". -func (f Face) Font() giofont.Font { - return f.font -} - -func gioStyle(s fontapi.Style) giofont.Style { - switch s { - case fontapi.StyleItalic: - return giofont.Italic - case fontapi.StyleNormal: - fallthrough - default: - return giofont.Regular - } -} - -func mdStyle(g giofont.Style) fontapi.Style { - switch g { - case giofont.Italic: - return fontapi.StyleItalic - case giofont.Regular: - fallthrough - default: - return fontapi.StyleNormal - } -} - -func gioWeight(w fontapi.Weight) giofont.Weight { - switch w { - case fontapi.WeightThin: - return giofont.Thin - case fontapi.WeightExtraLight: - return giofont.ExtraLight - case fontapi.WeightLight: - return giofont.Light - case fontapi.WeightNormal: - return giofont.Normal - case fontapi.WeightMedium: - return giofont.Medium - case fontapi.WeightSemibold: - return giofont.SemiBold - case fontapi.WeightBold: - return giofont.Bold - case fontapi.WeightExtraBold: - return giofont.ExtraBold - case fontapi.WeightBlack: - return giofont.Black - default: - return giofont.Normal - } -} - -func mdWeight(g giofont.Weight) fontapi.Weight { - switch g { - case giofont.Thin: - return fontapi.WeightThin - case giofont.ExtraLight: - return fontapi.WeightExtraLight - case giofont.Light: - return fontapi.WeightLight - case giofont.Normal: - return fontapi.WeightNormal - case giofont.Medium: - return fontapi.WeightMedium - case giofont.SemiBold: - return fontapi.WeightSemibold - case giofont.Bold: - return fontapi.WeightBold - case giofont.ExtraBold: - return fontapi.WeightExtraBold - case giofont.Black: - return fontapi.WeightBlack - default: - return fontapi.WeightNormal - } -} diff --git a/gio/gesture/gesture.go b/gio/gesture/gesture.go deleted file mode 100644 index b12a3f6..0000000 --- a/gio/gesture/gesture.go +++ /dev/null @@ -1,488 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package gesture implements common pointer gestures. - -Gestures accept low level pointer Events from an event -Queue and detect higher level actions such as clicks -and scrolling. -*/ -package gesture - -import ( - "image" - "math" - "runtime" - "time" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/fling" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// The duration is somewhat arbitrary. -const doubleClickDuration = 200 * time.Millisecond - -// Hover detects the hover gesture for a pointer area. -type Hover struct { - // entered tracks whether the pointer is inside the gesture. - entered bool - // pid is the pointer.ID. - pid pointer.ID -} - -// Add the gesture to detect hovering over the current pointer area. -func (h *Hover) Add(ops *op.Ops) { - event.Op(ops, h) -} - -// Update state and report whether a pointer is inside the area. -func (h *Hover) Update(q input.Source) bool { - for { - ev, ok := q.Event(pointer.Filter{ - Target: h, - Kinds: pointer.Enter | pointer.Leave | pointer.Cancel, - }) - if !ok { - break - } - e, ok := ev.(pointer.Event) - if !ok { - continue - } - switch e.Kind { - case pointer.Leave, pointer.Cancel: - if h.entered && h.pid == e.PointerID { - h.entered = false - } - case pointer.Enter: - if !h.entered { - h.pid = e.PointerID - } - if h.pid == e.PointerID { - h.entered = true - } - } - } - return h.entered -} - -// Click detects click gestures in the form -// of ClickEvents. -type Click struct { - // clickedAt is the timestamp at which - // the last click occurred. - clickedAt time.Duration - // clicks is incremented if successive clicks - // are performed within a fixed duration. - clicks int - // pressed tracks whether the pointer is pressed. - pressed bool - // hovered tracks whether the pointer is inside the gesture. - hovered bool - // entered tracks whether an Enter event has been received. - entered bool - // pid is the pointer.ID. - pid pointer.ID -} - -// ClickEvent represent a click action, either a -// KindPress for the beginning of a click or a -// KindClick for a completed click. -type ClickEvent struct { - Kind ClickKind - Position image.Point - Source pointer.Source - Modifiers key.Modifiers - // NumClicks records successive clicks occurring - // within a short duration of each other. - NumClicks int -} - -type ClickKind uint8 - -// Drag detects drag gestures in the form of pointer.Drag events. -type Drag struct { - dragging bool - pressed bool - pid pointer.ID - start f32.Point -} - -// Scroll detects scroll gestures and reduces them to -// scroll distances. Scroll recognizes mouse wheel -// movements as well as drag and fling touch gestures. -type Scroll struct { - dragging bool - estimator fling.Extrapolation - flinger fling.Animation - pid pointer.ID - last int - // Leftover scroll. - scroll float32 -} - -type ScrollState uint8 - -type Axis uint8 - -const ( - Horizontal Axis = iota - Vertical - Both -) - -const ( - // KindPress is reported for the first pointer - // press. - KindPress ClickKind = iota - // KindClick is reported when a click action - // is complete. - KindClick - // KindCancel is reported when the gesture is - // cancelled. - KindCancel -) - -const ( - // StateIdle is the default scroll state. - StateIdle ScrollState = iota - // StateDragging is reported during drag gestures. - StateDragging - // StateFlinging is reported when a fling is - // in progress. - StateFlinging -) - -const touchSlop = unit.Dp(3) - -// Add the handler to the operation list to receive click events. -func (c *Click) Add(ops *op.Ops) { - event.Op(ops, c) -} - -// Hovered returns whether a pointer is inside the area. -func (c *Click) Hovered() bool { - return c.hovered -} - -// Pressed returns whether a pointer is pressing. -func (c *Click) Pressed() bool { - return c.pressed -} - -// Update state and return the next click events, if any. -func (c *Click) Update(q input.Source) (ClickEvent, bool) { - for { - evt, ok := q.Event(pointer.Filter{ - Target: c, - Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave | pointer.Cancel, - }) - if !ok { - break - } - e, ok := evt.(pointer.Event) - if !ok { - continue - } - switch e.Kind { - case pointer.Release: - if !c.pressed || c.pid != e.PointerID { - break - } - c.pressed = false - if !c.entered || c.hovered { - return ClickEvent{ - Kind: KindClick, - Position: e.Position.Round(), - Source: e.Source, - Modifiers: e.Modifiers, - NumClicks: c.clicks, - }, true - } else { - return ClickEvent{Kind: KindCancel}, true - } - case pointer.Cancel: - wasPressed := c.pressed - c.pressed = false - c.hovered = false - c.entered = false - if wasPressed { - return ClickEvent{Kind: KindCancel}, true - } - case pointer.Press: - if c.pressed { - break - } - if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary { - break - } - if !c.hovered { - c.pid = e.PointerID - } - if c.pid != e.PointerID { - break - } - c.pressed = true - if e.Time-c.clickedAt < doubleClickDuration { - c.clicks++ - } else { - c.clicks = 1 - } - c.clickedAt = e.Time - return ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}, true - case pointer.Leave: - if !c.pressed { - c.pid = e.PointerID - } - if c.pid == e.PointerID { - c.hovered = false - } - case pointer.Enter: - if !c.pressed { - c.pid = e.PointerID - } - if c.pid == e.PointerID { - c.hovered = true - c.entered = true - } - } - } - return ClickEvent{}, false -} - -func (ClickEvent) ImplementsEvent() {} - -// Add the handler to the operation list to receive scroll events. -// The bounds variable refers to the scrolling boundaries -// as defined in [pointer.Filter]. -func (s *Scroll) Add(ops *op.Ops) { - event.Op(ops, s) -} - -// Stop any remaining fling movement. -func (s *Scroll) Stop() { - s.flinger = fling.Animation{} -} - -// Update state and report the scroll distance along axis. -func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, scrollx, scrolly pointer.ScrollRange) int { - total := 0 - f := pointer.Filter{ - Target: s, - Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel, - ScrollX: scrollx, - ScrollY: scrolly, - } - for { - evt, ok := q.Event(f) - if !ok { - break - } - e, ok := evt.(pointer.Event) - if !ok { - continue - } - switch e.Kind { - case pointer.Press: - if s.dragging { - break - } - // Only scroll on touch drags, or on Android where mice - // drags also scroll by convention. - if e.Source != pointer.Touch && runtime.GOOS != "android" { - break - } - s.Stop() - s.estimator = fling.Extrapolation{} - v := s.val(axis, e.Position) - s.last = int(math.Round(float64(v))) - s.estimator.Sample(e.Time, v) - s.dragging = true - s.pid = e.PointerID - case pointer.Release: - if s.pid != e.PointerID { - break - } - fling := s.estimator.Estimate() - if slop, d := float32(cfg.Dp(touchSlop)), fling.Distance; d < -slop || d > slop { - s.flinger.Start(cfg, t, fling.Velocity) - } - fallthrough - case pointer.Cancel: - s.dragging = false - case pointer.Scroll: - switch axis { - case Horizontal: - s.scroll += e.Scroll.X - case Vertical: - s.scroll += e.Scroll.Y - case Both: - s.scroll += e.Scroll.X + e.Scroll.Y - } - iscroll := int(s.scroll) - s.scroll -= float32(iscroll) - total += iscroll - case pointer.Drag: - if !s.dragging || s.pid != e.PointerID { - continue - } - val := s.val(axis, e.Position) - s.estimator.Sample(e.Time, val) - v := int(math.Round(float64(val))) - dist := s.last - v - if e.Priority < pointer.Grabbed { - slop := cfg.Dp(touchSlop) - if dist := dist; dist >= slop || -slop >= dist { - q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID}) - } - } else { - s.last = v - total += dist - } - } - } - total += s.flinger.Tick(t) - if s.flinger.Active() { - q.Execute(op.InvalidateCmd{}) - } - return total -} - -func (s *Scroll) val(axis Axis, p f32.Point) float32 { - switch axis { - case Horizontal: - return p.X - case Vertical: - return p.Y - case Both: - return p.X + p.Y - default: - return 0 - } -} - -// State reports the scroll state. -func (s *Scroll) State() ScrollState { - switch { - case s.flinger.Active(): - return StateFlinging - case s.dragging: - return StateDragging - default: - return StateIdle - } -} - -// Add the handler to the operation list to receive drag events. -func (d *Drag) Add(ops *op.Ops) { - event.Op(ops, d) -} - -// Update state and return the next drag event, if any. -func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event, bool) { - for { - ev, ok := q.Event(pointer.Filter{ - Target: d, - Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel, - }) - if !ok { - break - } - e, ok := ev.(pointer.Event) - if !ok { - continue - } - - switch e.Kind { - case pointer.Press: - if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) { - continue - } - d.pressed = true - if d.dragging { - continue - } - d.dragging = true - d.pid = e.PointerID - d.start = e.Position - case pointer.Drag: - if !d.dragging || e.PointerID != d.pid { - continue - } - switch axis { - case Horizontal: - e.Position.Y = d.start.Y - case Vertical: - e.Position.X = d.start.X - case Both: - // Do nothing - } - if e.Priority < pointer.Grabbed { - diff := e.Position.Sub(d.start) - slop := cfg.Dp(touchSlop) - if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) { - q.Execute(pointer.GrabCmd{Tag: d, ID: e.PointerID}) - } - } - case pointer.Release, pointer.Cancel: - d.pressed = false - if !d.dragging || e.PointerID != d.pid { - continue - } - d.dragging = false - } - - return e, true - } - - return pointer.Event{}, false -} - -// Dragging reports whether it is currently in use. -func (d *Drag) Dragging() bool { return d.dragging } - -// Pressed returns whether a pointer is pressing. -func (d *Drag) Pressed() bool { return d.pressed } - -func (a Axis) String() string { - switch a { - case Horizontal: - return "Horizontal" - case Vertical: - return "Vertical" - default: - panic("invalid Axis") - } -} - -func (ct ClickKind) String() string { - switch ct { - case KindPress: - return "KindPress" - case KindClick: - return "KindClick" - case KindCancel: - return "KindCancel" - default: - panic("invalid ClickKind") - } -} - -func (s ScrollState) String() string { - switch s { - case StateIdle: - return "StateIdle" - case StateDragging: - return "StateDragging" - case StateFlinging: - return "StateFlinging" - default: - panic("unreachable") - } -} diff --git a/gio/gesture/gesture_test.go b/gio/gesture/gesture_test.go deleted file mode 100644 index 3f07707..0000000 --- a/gio/gesture/gesture_test.go +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gesture - -import ( - "image" - "testing" - "time" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -func TestHover(t *testing.T) { - ops := new(op.Ops) - var h Hover - rect := image.Rect(20, 20, 40, 40) - stack := clip.Rect(rect).Push(ops) - h.Add(ops) - stack.Pop() - r := new(input.Router) - h.Update(r.Source()) - r.Frame(ops) - - r.Queue( - pointer.Event{Kind: pointer.Move, Position: f32.Pt(30, 30)}, - ) - if !h.Update(r.Source()) { - t.Fatal("expected hovered") - } - - r.Queue( - pointer.Event{Kind: pointer.Move, Position: f32.Pt(50, 50)}, - ) - if h.Update(r.Source()) { - t.Fatal("expected not hovered") - } -} - -func TestMouseClicks(t *testing.T) { - for _, tc := range []struct { - label string - events []event.Event - clicks []int // number of combined clicks per click (single, double...) - }{ - { - label: "single click", - events: mouseClickEvents(200 * time.Millisecond), - clicks: []int{1}, - }, - { - label: "double click", - events: mouseClickEvents( - 100*time.Millisecond, - 100*time.Millisecond+doubleClickDuration-1), - clicks: []int{1, 2}, - }, - { - label: "two single clicks", - events: mouseClickEvents( - 100*time.Millisecond, - 100*time.Millisecond+doubleClickDuration+1), - clicks: []int{1, 1}, - }, - } { - t.Run(tc.label, func(t *testing.T) { - var click Click - var ops op.Ops - click.Add(&ops) - - var r input.Router - click.Update(r.Source()) - r.Frame(&ops) - r.Queue(tc.events...) - - var clicks []ClickEvent - for { - ev, ok := click.Update(r.Source()) - if !ok { - break - } - if ev.Kind == KindClick { - clicks = append(clicks, ev) - } - } - if got, want := len(clicks), len(tc.clicks); got != want { - t.Fatalf("got %d mouse clicks, expected %d", got, want) - } - - for i, click := range clicks { - if got, want := click.NumClicks, tc.clicks[i]; got != want { - t.Errorf("got %d combined mouse clicks, expected %d", got, want) - } - } - }) - } -} - -func mouseClickEvents(times ...time.Duration) []event.Event { - press := pointer.Event{ - Kind: pointer.Press, - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - } - events := make([]event.Event, 0, 2*len(times)) - for _, t := range times { - press := press - press.Time = t - release := press - release.Kind = pointer.Release - events = append(events, press, release) - } - return events -} diff --git a/gio/gpu/api.go b/gio/gpu/api.go deleted file mode 100644 index 1f0ff04..0000000 --- a/gio/gpu/api.go +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gpu - -import "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" - -// An API carries the necessary GPU API specific resources to create a Device. -// There is an API type for each supported GPU API such as OpenGL and Direct3D. -type API = driver.API - -// A RenderTarget denotes the destination framebuffer for a frame. -type RenderTarget = driver.RenderTarget - -// OpenGLRenderTarget is a render target suitable for the OpenGL backend. -type OpenGLRenderTarget = driver.OpenGLRenderTarget - -// Direct3D11RenderTarget is a render target suitable for the Direct3D 11 backend. -type Direct3D11RenderTarget = driver.Direct3D11RenderTarget - -// MetalRenderTarget is a render target suitable for the Metal backend. -type MetalRenderTarget = driver.MetalRenderTarget - -// VulkanRenderTarget is a render target suitable for the Vulkan backend. -type VulkanRenderTarget = driver.VulkanRenderTarget - -// OpenGL denotes the OpenGL or OpenGL ES API. -type OpenGL = driver.OpenGL - -// Direct3D11 denotes the Direct3D API. -type Direct3D11 = driver.Direct3D11 - -// Metal denotes the Apple Metal API. -type Metal = driver.Metal - -// Vulkan denotes the Vulkan API. -type Vulkan = driver.Vulkan - -// ErrDeviceLost is returned from GPU operations when the underlying GPU device -// is lost and should be recreated. -var ErrDeviceLost = driver.ErrDeviceLost diff --git a/gio/gpu/caches.go b/gio/gpu/caches.go deleted file mode 100644 index 92fb811..0000000 --- a/gio/gpu/caches.go +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gpu - -import ( - "fmt" - "image" -) - -type textureCacheKey struct { - filter byte - handle any -} - -type textureCache struct { - res map[textureCacheKey]resourceCacheValue -} - -type resourceCacheValue struct { - used bool - resource resource -} - -// opCache is like a resourceCache but using concrete types and a -// freelist instead of two maps to avoid runtime.mapaccess2 calls -// since benchmarking showed them as a bottleneck. -type opCache struct { - // store the index + 1 in cache this key is stored in - index map[opKey]int - // list of indexes in cache that are free and can be used - freelist []int - cache []opCacheValue -} - -type opCacheValue struct { - data pathData - - bounds image.Rectangle - // the fields below are handled by opCache - key opKey - keep bool -} - -func newTextureCache() *textureCache { - return &textureCache{ - res: make(map[textureCacheKey]resourceCacheValue), - } -} - -func (r *textureCache) get(key textureCacheKey) (resource, bool) { - v, exists := r.res[key] - if !exists { - return nil, false - } - if !v.used { - v.used = true - r.res[key] = v - } - return v.resource, exists -} - -func (r *textureCache) put(key textureCacheKey, val resource) { - v, exists := r.res[key] - if exists && v.used { - panic(fmt.Errorf("key exists, %v", key)) - } - v.used = true - v.resource = val - r.res[key] = v -} - -func (r *textureCache) frame() { - for k, v := range r.res { - if v.used { - v.used = false - r.res[k] = v - } else { - delete(r.res, k) - v.resource.release() - } - } -} - -func (r *textureCache) release() { - for _, v := range r.res { - v.resource.release() - } - r.res = nil -} - -func newOpCache() *opCache { - return &opCache{ - index: make(map[opKey]int), - freelist: make([]int, 0), - cache: make([]opCacheValue, 0), - } -} - -func (r *opCache) get(key opKey) (o opCacheValue, exist bool) { - v := r.index[key] - if v == 0 { - return - } - r.cache[v-1].keep = true - return r.cache[v-1], true -} - -func (r *opCache) put(key opKey, val opCacheValue) { - v := r.index[key] - val.keep = true - val.key = key - if v == 0 { - // not in cache - i := len(r.cache) - if len(r.freelist) > 0 { - i = r.freelist[len(r.freelist)-1] - r.freelist = r.freelist[:len(r.freelist)-1] - r.cache[i] = val - } else { - r.cache = append(r.cache, val) - } - r.index[key] = i + 1 - } else { - r.cache[v-1] = val - } -} - -func (r *opCache) frame() { - r.freelist = r.freelist[:0] - for i, v := range r.cache { - r.cache[i].keep = false - if v.keep { - continue - } - if v.data.data != nil { - v.data.release() - r.cache[i].data.data = nil - } - delete(r.index, v.key) - r.freelist = append(r.freelist, i) - } -} - -func (r *opCache) release() { - for i := range r.cache { - r.cache[i].keep = false - } - r.frame() - r.index = nil - r.freelist = nil - r.cache = nil -} diff --git a/gio/gpu/clip.go b/gio/gpu/clip.go deleted file mode 100644 index c02c198..0000000 --- a/gio/gpu/clip.go +++ /dev/null @@ -1,146 +0,0 @@ -package gpu - -import ( - "encoding/binary" - "math" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/stroke" -) - -type quadSplitter struct { - bounds f32.Rectangle - contour uint32 - d *drawOps - - // scratch space used by calls to stroke.SplitCubic - scratch []stroke.QuadSegment -} - -func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) { - // inlined code: - // encodeVertex(data, meta, 1, -1, from, ctrl, to) - // encodeVertex(data[vertStride:], meta, 1, 1, from, ctrl, to) - // encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to) - // encodeVertex(data[vertStride*3:], meta, -1, 1, from, ctrl, to) - // this code needs to stay in sync with `vertex.encode`. - - bo := binary.LittleEndian - data = data[:vertStride*4] - - // encode the main template - bo.PutUint32(data[4:8], meta) - bo.PutUint32(data[8:12], math.Float32bits(from.X)) - bo.PutUint32(data[12:16], math.Float32bits(from.Y)) - bo.PutUint32(data[16:20], math.Float32bits(ctrl.X)) - bo.PutUint32(data[20:24], math.Float32bits(ctrl.Y)) - bo.PutUint32(data[24:28], math.Float32bits(to.X)) - bo.PutUint32(data[28:32], math.Float32bits(to.Y)) - - copy(data[vertStride*1:vertStride*2], data[vertStride*0:vertStride*1]) - copy(data[vertStride*2:vertStride*3], data[vertStride*0:vertStride*1]) - copy(data[vertStride*3:vertStride*4], data[vertStride*0:vertStride*1]) - - bo.PutUint32(data[vertStride*0:vertStride*0+4], math.Float32bits(nwCorner)) - bo.PutUint32(data[vertStride*1:vertStride*1+4], math.Float32bits(neCorner)) - bo.PutUint32(data[vertStride*2:vertStride*2+4], math.Float32bits(swCorner)) - bo.PutUint32(data[vertStride*3:vertStride*3+4], math.Float32bits(seCorner)) -} - -const ( - nwCorner = 1*0.5 + 0*0.25 - neCorner = 1*0.5 + 1*0.25 - swCorner = 0*0.5 + 0*0.25 - seCorner = 0*0.5 + 1*0.25 -) - -func encodeVertex(data []byte, meta uint32, cornerx, cornery int16, from, ctrl, to f32.Point) { - var corner float32 - if cornerx == 1 { - corner += .5 - } - if cornery == 1 { - corner += .25 - } - v := vertex{ - Corner: corner, - FromX: from.X, - FromY: from.Y, - CtrlX: ctrl.X, - CtrlY: ctrl.Y, - ToX: to.X, - ToY: to.Y, - } - v.encode(data, meta) -} - -func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) { - data := qs.d.writeVertCache(vertStride * 4) - encodeQuadTo(data, qs.contour, from, ctrl, to) -} - -func (qs *quadSplitter) splitAndEncode(quad stroke.QuadSegment) { - cbnd := f32.Rectangle{ - Min: quad.From, - Max: quad.To, - }.Canon() - from, ctrl, to := quad.From, quad.Ctrl, quad.To - - // If the curve contain areas where a vertical line - // intersects it twice, split the curve in two x monotone - // lower and upper curves. The stencil fragment program - // expects only one intersection per curve. - - // Find the t where the derivative in x is 0. - v0 := ctrl.Sub(from) - v1 := to.Sub(ctrl) - d := v0.X - v1.X - // t = v0 / d. Split if t is in ]0;1[. - if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { - t := v0.X / d - ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t)) - ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) - mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) - qs.encodeQuadTo(from, ctrl0, mid) - qs.encodeQuadTo(mid, ctrl1, to) - if mid.X > cbnd.Max.X { - cbnd.Max.X = mid.X - } - if mid.X < cbnd.Min.X { - cbnd.Min.X = mid.X - } - } else { - qs.encodeQuadTo(from, ctrl, to) - } - // Find the y extremum, if any. - d = v0.Y - v1.Y - if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { - t := v0.Y / d - y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y - if y > cbnd.Max.Y { - cbnd.Max.Y = y - } - if y < cbnd.Min.Y { - cbnd.Min.Y = y - } - } - - qs.bounds = unionRect(qs.bounds, cbnd) -} - -// Union is like f32.Rectangle.Union but ignores empty rectangles. -func unionRect(r, s f32.Rectangle) f32.Rectangle { - if r.Min.X > s.Min.X { - r.Min.X = s.Min.X - } - if r.Min.Y > s.Min.Y { - r.Min.Y = s.Min.Y - } - if r.Max.X < s.Max.X { - r.Max.X = s.Max.X - } - if r.Max.Y < s.Max.Y { - r.Max.Y = s.Max.Y - } - return r -} diff --git a/gio/gpu/clip_test.go b/gio/gpu/clip_test.go deleted file mode 100644 index 7d64731..0000000 --- a/gio/gpu/clip_test.go +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gpu - -import ( - "testing" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32" -) - -func BenchmarkEncodeQuadTo(b *testing.B) { - var data [vertStride * 4]byte - for i := 0; b.Loop(); i++ { - v := float32(i) - encodeQuadTo(data[:], 123, - f32.Point{X: v, Y: v}, - f32.Point{X: v, Y: v}, - f32.Point{X: v, Y: v}, - ) - } -} diff --git a/gio/gpu/gpu.go b/gio/gpu/gpu.go deleted file mode 100644 index 7011c5a..0000000 --- a/gio/gpu/gpu.go +++ /dev/null @@ -1,1603 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package gpu implements the rendering of Gio drawing operations. It -is used by package app and package app/headless and is otherwise not -useful except for integrating with external window implementations. -*/ -package gpu - -import ( - "encoding/binary" - "errors" - "fmt" - "image" - "image/color" - "math" - "reflect" - "slices" - "time" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" - "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" - "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/internal/scene" - "github.com/p9c/p9/pkg/gel/gio/internal/stroke" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/shader" - "github.com/p9c/p9/pkg/gel/gio/shader/gio" - - // Register backends. - _ "github.com/p9c/p9/pkg/gel/gio/gpu/internal/d3d11" - _ "github.com/p9c/p9/pkg/gel/gio/gpu/internal/metal" - _ "github.com/p9c/p9/pkg/gel/gio/gpu/internal/opengl" - _ "github.com/p9c/p9/pkg/gel/gio/gpu/internal/vulkan" -) - -type GPU interface { - // Release non-Go resources. The GPU is no longer valid after Release. - Release() - // Clear sets the clear color for the next Frame. - Clear(color color.NRGBA) - // Frame draws the graphics operations from op into a viewport of target. - Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error -} - -type gpu struct { - cache *textureCache - - profile string - timers *timers - frameStart time.Time - stencilTimer, coverTimer, cleanupTimer *timer - drawOps drawOps - ctx driver.Device - renderer *renderer -} - -type renderer struct { - ctx driver.Device - blitter *blitter - pather *pather - packer packer - intersections packer - layers packer - layerFBOs fboSet -} - -type drawOps struct { - reader ops.Reader - states []f32.Affine2D - transStack []f32.Affine2D - layers []opacityLayer - opacityStack []int - vertCache []byte - viewport image.Point - clear bool - clearColor f32color.RGBA - imageOps []imageOp - pathOps []*pathOp - pathOpCache []pathOp - qs quadSplitter - pathCache *opCache -} - -type opacityLayer struct { - opacity float32 - parent int - // depth of the opacity stack. Layers of equal depth are - // independent and may be packed into one atlas. - depth int - // opStart and opEnd denote the range of drawOps.imageOps - // that belong to the layer. - opStart, opEnd int - // clip of the layer operations. - clip image.Rectangle - place placement -} - -type drawState struct { - t f32.Affine2D - cpath *pathOp - - matType materialType - // Current paint.ImageOp - image imageOpData - // Current paint.ColorOp, if any. - color color.NRGBA - - // Current paint.LinearGradientOp. - stop1 f32.Point - stop2 f32.Point - color1 color.NRGBA - color2 color.NRGBA -} - -type pathOp struct { - off image.Point - // rect tracks whether the clip stack can be represented by a - // pixel-aligned rectangle. - rect bool - // clip is the union of all - // later clip rectangles. - clip image.Rectangle - bounds image.Rectangle - // intersect is the intersection of bounds and all - // previous clip bounds. - intersect image.Rectangle - pathKey opKey - path bool - pathVerts []byte - parent *pathOp - place placement -} - -type imageOp struct { - path *pathOp - clip image.Rectangle - material material - clipType clipType - // place is either a placement in the path fbos or intersection fbos, - // depending on clipType. - place placement - // layerOps is the number of operations this - // operation replaces. - layerOps int -} - -func decodeStrokeOp(data []byte) float32 { - _ = data[4] - bo := binary.LittleEndian - return math.Float32frombits(bo.Uint32(data[1:])) -} - -type quadsOp struct { - key opKey - aux []byte -} - -type opKey struct { - outline bool - strokeWidth float32 - sx, hx, sy, hy float32 - ops.Key -} - -type material struct { - material materialType - opaque bool - // For materialTypeColor. - color f32color.RGBA - // For materialTypeLinearGradient. - color1 f32color.RGBA - color2 f32color.RGBA - opacity float32 - // For materialTypeTexture. - data imageOpData - tex driver.Texture - uvTrans f32.Affine2D -} - -const ( - filterLinear = 0 - filterNearest = 1 -) - -// imageOpData is the shadow of paint.ImageOp. -type imageOpData struct { - src *image.RGBA - handle any - filter byte -} - -type linearGradientOpData struct { - stop1 f32.Point - color1 color.NRGBA - stop2 f32.Point - color2 color.NRGBA -} - -func decodeImageOp(data []byte, refs []any) imageOpData { - handle := refs[1] - if handle == nil { - return imageOpData{} - } - return imageOpData{ - src: refs[0].(*image.RGBA), - handle: handle, - filter: data[1], - } -} - -func decodeColorOp(data []byte) color.NRGBA { - data = data[:ops.TypeColorLen] - return color.NRGBA{ - R: data[1], - G: data[2], - B: data[3], - A: data[4], - } -} - -func decodeLinearGradientOp(data []byte) linearGradientOpData { - data = data[:ops.TypeLinearGradientLen] - bo := binary.LittleEndian - return linearGradientOpData{ - stop1: f32.Point{ - X: math.Float32frombits(bo.Uint32(data[1:])), - Y: math.Float32frombits(bo.Uint32(data[5:])), - }, - stop2: f32.Point{ - X: math.Float32frombits(bo.Uint32(data[9:])), - Y: math.Float32frombits(bo.Uint32(data[13:])), - }, - color1: color.NRGBA{ - R: data[17+0], - G: data[17+1], - B: data[17+2], - A: data[17+3], - }, - color2: color.NRGBA{ - R: data[21+0], - G: data[21+1], - B: data[21+2], - A: data[21+3], - }, - } -} - -type resource interface { - release() -} - -type texture struct { - src *image.RGBA - tex driver.Texture -} - -type blitter struct { - ctx driver.Device - viewport image.Point - pipelines [2][3]*pipeline - colUniforms *blitColUniforms - texUniforms *blitTexUniforms - linearGradientUniforms *blitLinearGradientUniforms - quadVerts driver.Buffer -} - -type blitColUniforms struct { - blitUniforms - _ [128 - unsafe.Sizeof(blitUniforms{}) - unsafe.Sizeof(colorUniforms{})]byte // Padding to 128 bytes. - colorUniforms -} - -type blitTexUniforms struct { - blitUniforms -} - -type blitLinearGradientUniforms struct { - blitUniforms - _ [128 - unsafe.Sizeof(blitUniforms{}) - unsafe.Sizeof(gradientUniforms{})]byte // Padding to 128 bytes. - gradientUniforms -} - -type uniformBuffer struct { - buf driver.Buffer - ptr []byte -} - -type pipeline struct { - pipeline driver.Pipeline - uniforms *uniformBuffer -} - -type blitUniforms struct { - transform [4]float32 - uvTransformR1 [4]float32 - uvTransformR2 [4]float32 - opacity float32 - fbo float32 - _ [2]float32 -} - -type colorUniforms struct { - color f32color.RGBA -} - -type gradientUniforms struct { - color1 f32color.RGBA - color2 f32color.RGBA -} - -type clipType uint8 - -const ( - clipTypeNone clipType = iota - clipTypePath - clipTypeIntersection -) - -type materialType uint8 - -const ( - materialColor materialType = iota - materialLinearGradient - materialTexture -) - -// New creates a GPU for the given API. -func New(api API) (GPU, error) { - d, err := driver.NewDevice(api) - if err != nil { - return nil, err - } - return NewWithDevice(d) -} - -// NewWithDevice creates a GPU with a pre-existing device. -// -// Note: for internal use only. -func NewWithDevice(d driver.Device) (GPU, error) { - d.BeginFrame(nil, false, image.Point{}) - defer d.EndFrame() - feats := d.Caps().Features - switch { - case feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB): - return newGPU(d) - } - return nil, errors.New("no available GPU driver") -} - -func newGPU(ctx driver.Device) (*gpu, error) { - g := &gpu{ - cache: newTextureCache(), - } - g.drawOps.pathCache = newOpCache() - if err := g.init(ctx); err != nil { - return nil, err - } - return g, nil -} - -func (g *gpu) init(ctx driver.Device) error { - g.ctx = ctx - g.renderer = newRenderer(ctx) - return nil -} - -func (g *gpu) Clear(col color.NRGBA) { - g.drawOps.clear = true - g.drawOps.clearColor = f32color.LinearFromSRGB(col) -} - -func (g *gpu) Release() { - g.renderer.release() - g.drawOps.pathCache.release() - g.cache.release() - if g.timers != nil { - g.timers.Release() - } - g.ctx.Release() -} - -func (g *gpu) Frame(frameOps *op.Ops, target RenderTarget, viewport image.Point) error { - g.collect(viewport, frameOps) - return g.frame(target) -} - -func (g *gpu) collect(viewport image.Point, frameOps *op.Ops) { - g.renderer.blitter.viewport = viewport - g.renderer.pather.viewport = viewport - g.drawOps.reset(viewport) - g.drawOps.collect(frameOps, viewport) - if false && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) { - g.frameStart = time.Now() - g.timers = newTimers(g.ctx) - g.stencilTimer = g.timers.newTimer() - g.coverTimer = g.timers.newTimer() - g.cleanupTimer = g.timers.newTimer() - } -} - -func (g *gpu) frame(target RenderTarget) error { - viewport := g.renderer.blitter.viewport - defFBO := g.ctx.BeginFrame(target, g.drawOps.clear, viewport) - defer g.ctx.EndFrame() - g.drawOps.buildPaths(g.ctx) - for _, img := range g.drawOps.imageOps { - expandPathOp(img.path, img.clip) - } - g.stencilTimer.begin() - g.renderer.packStencils(&g.drawOps.pathOps) - g.renderer.stencilClips(g.drawOps.pathCache, g.drawOps.pathOps) - g.renderer.packIntersections(g.drawOps.imageOps) - g.renderer.prepareIntersections(g.drawOps.imageOps) - g.renderer.intersect(g.drawOps.imageOps) - g.stencilTimer.end() - g.coverTimer.begin() - g.renderer.uploadImages(g.cache, g.drawOps.imageOps) - g.renderer.prepareDrawOps(g.drawOps.imageOps) - g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers) - g.renderer.drawLayers(g.drawOps.layers, g.drawOps.imageOps) - d := driver.LoadDesc{ - ClearColor: g.drawOps.clearColor, - } - if g.drawOps.clear { - g.drawOps.clear = false - d.Action = driver.LoadActionClear - } - g.ctx.BeginRenderPass(defFBO, d) - g.ctx.Viewport(0, 0, viewport.X, viewport.Y) - g.renderer.drawOps(false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps) - g.coverTimer.end() - g.ctx.EndRenderPass() - g.cleanupTimer.begin() - g.cache.frame() - g.drawOps.pathCache.frame() - g.cleanupTimer.end() - if false && g.timers.ready() { - st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed - ft := st + covt + cleant - q := 100 * time.Microsecond - st, covt = st.Round(q), covt.Round(q) - frameDur := time.Since(g.frameStart).Round(q) - ft = ft.Round(q) - g.profile = fmt.Sprintf("draw:%7s gpu:%7s st:%7s cov:%7s", frameDur, ft, st, covt) - } - return nil -} - -func (g *gpu) Profile() string { - return g.profile -} - -func (r *renderer) texHandle(cache *textureCache, data imageOpData) driver.Texture { - key := textureCacheKey{ - filter: data.filter, - handle: data.handle, - } - - var tex *texture - t, exists := cache.get(key) - if !exists { - t = &texture{ - src: data.src, - } - cache.put(key, t) - } - tex = t.(*texture) - if tex.tex != nil { - return tex.tex - } - - var minFilter, magFilter driver.TextureFilter - switch data.filter { - case filterLinear: - minFilter, magFilter = driver.FilterLinearMipmapLinear, driver.FilterLinear - case filterNearest: - minFilter, magFilter = driver.FilterNearest, driver.FilterNearest - } - - handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA, - data.src.Bounds().Dx(), data.src.Bounds().Dy(), - minFilter, magFilter, - driver.BufferBindingTexture, - ) - if err != nil { - panic(err) - } - driver.UploadImage(handle, image.Pt(0, 0), data.src) - tex.tex = handle - return tex.tex -} - -func (t *texture) release() { - if t.tex != nil { - t.tex.Release() - } -} - -func newRenderer(ctx driver.Device) *renderer { - r := &renderer{ - ctx: ctx, - blitter: newBlitter(ctx), - pather: newPather(ctx), - } - - maxDim := ctx.Caps().MaxTextureSize - // Large atlas textures cause artifacts due to precision loss in - // shaders. - if cap := 8192; maxDim > cap { - maxDim = cap - } - d := image.Pt(maxDim, maxDim) - - r.packer.maxDims = d - r.intersections.maxDims = d - r.layers.maxDims = d - return r -} - -func (r *renderer) release() { - r.pather.release() - r.blitter.release() - r.layerFBOs.delete(r.ctx, 0) -} - -func newBlitter(ctx driver.Device) *blitter { - quadVerts, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, - byteslice.Slice([]float32{ - -1, -1, 0, 0, - +1, -1, 1, 0, - -1, +1, 0, 1, - +1, +1, 1, 1, - }), - ) - if err != nil { - panic(err) - } - b := &blitter{ - ctx: ctx, - quadVerts: quadVerts, - } - b.colUniforms = new(blitColUniforms) - b.texUniforms = new(blitTexUniforms) - b.linearGradientUniforms = new(blitLinearGradientUniforms) - pipelines, err := createColorPrograms(ctx, gio.Shader_blit_vert, gio.Shader_blit_frag, - [3]any{b.colUniforms, b.linearGradientUniforms, b.texUniforms}, - ) - if err != nil { - panic(err) - } - b.pipelines = pipelines - return b -} - -func (b *blitter) release() { - b.quadVerts.Release() - for _, p := range b.pipelines { - for _, p := range p { - p.Release() - } - } -} - -func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]any) (pipelines [2][3]*pipeline, err error) { - defer func() { - if err != nil { - for _, p := range pipelines { - for _, p := range p { - if p != nil { - p.Release() - } - } - } - } - }() - blend := driver.BlendDesc{ - Enable: true, - SrcFactor: driver.BlendFactorOne, - DstFactor: driver.BlendFactorOneMinusSrcAlpha, - } - layout := driver.VertexLayout{ - Inputs: []driver.InputDesc{ - {Type: shader.DataTypeFloat, Size: 2, Offset: 0}, - {Type: shader.DataTypeFloat, Size: 2, Offset: 4 * 2}, - }, - Stride: 4 * 4, - } - vsh, err := b.NewVertexShader(vsSrc) - if err != nil { - return pipelines, err - } - defer vsh.Release() - for i, format := range []driver.TextureFormat{driver.TextureFormatOutput, driver.TextureFormatSRGBA} { - { - fsh, err := b.NewFragmentShader(fsSrc[materialTexture]) - if err != nil { - return pipelines, err - } - defer fsh.Release() - pipe, err := b.NewPipeline(driver.PipelineDesc{ - VertexShader: vsh, - FragmentShader: fsh, - BlendDesc: blend, - VertexLayout: layout, - PixelFormat: format, - Topology: driver.TopologyTriangleStrip, - }) - if err != nil { - return pipelines, err - } - var vertBuffer *uniformBuffer - if u := uniforms[materialTexture]; u != nil { - vertBuffer = newUniformBuffer(b, u) - } - pipelines[i][materialTexture] = &pipeline{pipe, vertBuffer} - } - { - var vertBuffer *uniformBuffer - fsh, err := b.NewFragmentShader(fsSrc[materialColor]) - if err != nil { - return pipelines, err - } - defer fsh.Release() - pipe, err := b.NewPipeline(driver.PipelineDesc{ - VertexShader: vsh, - FragmentShader: fsh, - BlendDesc: blend, - VertexLayout: layout, - PixelFormat: format, - Topology: driver.TopologyTriangleStrip, - }) - if err != nil { - return pipelines, err - } - if u := uniforms[materialColor]; u != nil { - vertBuffer = newUniformBuffer(b, u) - } - pipelines[i][materialColor] = &pipeline{pipe, vertBuffer} - } - { - var vertBuffer *uniformBuffer - fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient]) - if err != nil { - return pipelines, err - } - defer fsh.Release() - pipe, err := b.NewPipeline(driver.PipelineDesc{ - VertexShader: vsh, - FragmentShader: fsh, - BlendDesc: blend, - VertexLayout: layout, - PixelFormat: format, - Topology: driver.TopologyTriangleStrip, - }) - if err != nil { - return pipelines, err - } - if u := uniforms[materialLinearGradient]; u != nil { - vertBuffer = newUniformBuffer(b, u) - } - pipelines[i][materialLinearGradient] = &pipeline{pipe, vertBuffer} - } - } - return pipelines, nil -} - -func (r *renderer) stencilClips(pathCache *opCache, ops []*pathOp) { - if len(r.packer.sizes) == 0 { - return - } - fbo := -1 - r.pather.begin(r.packer.sizes) - for _, p := range ops { - if fbo != p.place.Idx { - if fbo != -1 { - r.ctx.EndRenderPass() - } - fbo = p.place.Idx - f := r.pather.stenciler.cover(fbo) - r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear}) - r.ctx.BindPipeline(r.pather.stenciler.pipeline.pipeline.pipeline) - r.ctx.BindIndexBuffer(r.pather.stenciler.indexBuf) - } - v, _ := pathCache.get(p.pathKey) - r.pather.stencilPath(p.clip, p.off, p.place.Pos, v.data) - } - if fbo != -1 { - r.ctx.EndRenderPass() - } -} - -func (r *renderer) prepareIntersections(ops []imageOp) { - for _, img := range ops { - if img.clipType != clipTypeIntersection { - continue - } - fbo := r.pather.stenciler.cover(img.path.place.Idx) - r.ctx.PrepareTexture(fbo.tex) - } -} - -func (r *renderer) intersect(ops []imageOp) { - if len(r.intersections.sizes) == 0 { - return - } - fbo := -1 - r.pather.stenciler.beginIntersect(r.intersections.sizes) - for _, img := range ops { - if img.clipType != clipTypeIntersection { - continue - } - if fbo != img.place.Idx { - if fbo != -1 { - r.ctx.EndRenderPass() - } - fbo = img.place.Idx - f := r.pather.stenciler.intersections.fbos[fbo] - d := driver.LoadDesc{Action: driver.LoadActionClear} - d.ClearColor.R = 1.0 - r.ctx.BeginRenderPass(f.tex, d) - r.ctx.BindPipeline(r.pather.stenciler.ipipeline.pipeline.pipeline) - r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0) - } - r.ctx.Viewport(img.place.Pos.X, img.place.Pos.Y, img.clip.Dx(), img.clip.Dy()) - r.intersectPath(img.path, img.clip) - } - if fbo != -1 { - r.ctx.EndRenderPass() - } -} - -func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) { - if p.parent != nil { - r.intersectPath(p.parent, clip) - } - if !p.path { - return - } - uv := image.Rectangle{ - Min: p.place.Pos, - Max: p.place.Pos.Add(p.clip.Size()), - } - o := clip.Min.Sub(p.clip.Min) - sub := image.Rectangle{ - Min: o, - Max: o.Add(clip.Size()), - } - fbo := r.pather.stenciler.cover(p.place.Idx) - r.ctx.BindTexture(0, fbo.tex) - coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size) - subScale, subOff := texSpaceTransform(f32.FRect(sub), p.clip.Size()) - r.pather.stenciler.ipipeline.uniforms.vert.uvTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y} - r.pather.stenciler.ipipeline.uniforms.vert.subUVTransform = [4]float32{subScale.X, subScale.Y, subOff.X, subOff.Y} - r.pather.stenciler.ipipeline.pipeline.UploadUniforms(r.ctx) - r.ctx.DrawArrays(0, 4) -} - -func (r *renderer) packIntersections(ops []imageOp) { - r.intersections.clear() - for i, img := range ops { - var npaths int - var onePath *pathOp - for p := img.path; p != nil; p = p.parent { - if p.path { - onePath = p - npaths++ - } - } - switch npaths { - case 0: - case 1: - place := onePath.place - place.Pos = place.Pos.Sub(onePath.clip.Min).Add(img.clip.Min) - ops[i].place = place - ops[i].clipType = clipTypePath - default: - sz := image.Point{X: img.clip.Dx(), Y: img.clip.Dy()} - place, ok := r.intersections.add(sz) - if !ok { - panic("internal error: if the intersection fit, the intersection should fit as well") - } - ops[i].clipType = clipTypeIntersection - ops[i].place = place - } - } -} - -func (r *renderer) packStencils(pops *[]*pathOp) { - r.packer.clear() - ops := *pops - // Allocate atlas space for cover textures. - var i int - for i < len(ops) { - p := ops[i] - if p.clip.Empty() { - ops[i] = ops[len(ops)-1] - ops = ops[:len(ops)-1] - continue - } - place, ok := r.packer.add(p.clip.Size()) - if !ok { - // The clip area is at most the entire screen. Hopefully no - // screen is larger than GL_MAX_TEXTURE_SIZE. - panic(fmt.Errorf("clip area %v is larger than maximum texture size %v", p.clip, r.packer.maxDims)) - } - p.place = place - i++ - } - *pops = ops -} - -func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer { - // Make every layer bounds contain nested layers; cull empty layers. - for i := len(layers) - 1; i >= 0; i-- { - l := layers[i] - if l.parent != -1 { - b := layers[l.parent].clip - layers[l.parent].clip = b.Union(l.clip) - } - if l.clip.Empty() { - layers = slices.Delete(layers, i, i+1) - } - } - // Pack layers. - r.layers.clear() - depth := 0 - for i := range layers { - l := &layers[i] - // Only layers of the same depth may be packed together. - if l.depth != depth { - r.layers.newPage() - } - place, ok := r.layers.add(l.clip.Size()) - if !ok { - // The layer area is at most the entire screen. Hopefully no - // screen is larger than GL_MAX_TEXTURE_SIZE. - panic(fmt.Errorf("layer size %v is larger than maximum texture size %v", l.clip.Size(), r.layers.maxDims)) - } - l.place = place - } - return layers -} - -func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) { - if len(r.layers.sizes) == 0 { - return - } - fbo := -1 - r.layerFBOs.resize(r.ctx, driver.TextureFormatSRGBA, r.layers.sizes) - for i := len(layers) - 1; i >= 0; i-- { - l := layers[i] - if fbo != l.place.Idx { - if fbo != -1 { - r.ctx.EndRenderPass() - r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex) - } - fbo = l.place.Idx - f := r.layerFBOs.fbos[fbo] - r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear}) - } - v := image.Rectangle{ - Min: l.place.Pos, - Max: l.place.Pos.Add(l.clip.Size()), - } - r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy()) - f := r.layerFBOs.fbos[fbo] - r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd]) - sr := f32.FRect(v) - uvScale, uvOffset := texSpaceTransform(sr, f.size) - uvTrans := f32.AffineId().Scale(f32.Point{}, uvScale).Offset(uvOffset) - // Replace layer ops with one textured op. - ops[l.opStart] = imageOp{ - clip: l.clip, - material: material{ - material: materialTexture, - tex: f.tex, - uvTrans: uvTrans, - opacity: l.opacity, - }, - layerOps: l.opEnd - l.opStart - 1, - } - } - if fbo != -1 { - r.ctx.EndRenderPass() - r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex) - } -} - -func (d *drawOps) reset(viewport image.Point) { - d.viewport = viewport - d.imageOps = d.imageOps[:0] - d.pathOps = d.pathOps[:0] - d.pathOpCache = d.pathOpCache[:0] - d.vertCache = d.vertCache[:0] - d.transStack = d.transStack[:0] - d.layers = d.layers[:0] - d.opacityStack = d.opacityStack[:0] -} - -func (d *drawOps) collect(root *op.Ops, viewportSize image.Point) { - viewport := image.Rectangle{Max: viewportSize} - var ops *ops.Ops - if root != nil { - ops = &root.Internal - } - d.reader.Reset(ops) - d.collectOps(&d.reader, viewport) -} - -func (d *drawOps) buildPaths(ctx driver.Device) { - for _, p := range d.pathOps { - if v, exists := d.pathCache.get(p.pathKey); !exists || v.data.data == nil { - data := buildPath(ctx, p.pathVerts) - d.pathCache.put(p.pathKey, opCacheValue{ - data: data, - bounds: p.bounds, - }) - } - p.pathVerts = nil - } -} - -func (d *drawOps) newPathOp() *pathOp { - d.pathOpCache = append(d.pathOpCache, pathOp{}) - return &d.pathOpCache[len(d.pathOpCache)-1] -} - -func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds image.Rectangle, off image.Point) { - npath := d.newPathOp() - *npath = pathOp{ - parent: state.cpath, - bounds: bounds, - off: off, - intersect: bounds.Add(off), - rect: true, - } - if npath.parent != nil { - npath.rect = npath.parent.rect - npath.intersect = npath.parent.intersect.Intersect(npath.intersect) - } - if len(aux) > 0 { - npath.rect = false - npath.pathKey = auxKey - npath.path = true - npath.pathVerts = aux - d.pathOps = append(d.pathOps, npath) - } - state.cpath = npath -} - -func (d *drawOps) save(id int, state f32.Affine2D) { - if extra := id - len(d.states) + 1; extra > 0 { - for range extra { - d.states = append(d.states, f32.AffineId()) - } - } - d.states[id] = state -} - -func (k opKey) SetTransform(t f32.Affine2D) opKey { - sx, hx, _, hy, sy, _ := t.Elems() - k.sx = sx - k.hx = hx - k.hy = hy - k.sy = sy - return k -} - -func (d *drawOps) collectOps(r *ops.Reader, viewport image.Rectangle) { - var quads quadsOp - state := drawState{ - t: f32.AffineId(), - } - reset := func() { - state = drawState{ - t: f32.AffineId(), - color: color.NRGBA{A: 0xff}, - } - } - reset() -loop: - for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { - switch ops.OpType(encOp.Data[0]) { - case ops.TypeTransform: - dop, push := ops.DecodeTransform(encOp.Data) - if push { - d.transStack = append(d.transStack, state.t) - } - state.t = state.t.Mul(dop) - case ops.TypePopTransform: - n := len(d.transStack) - state.t = d.transStack[n-1] - d.transStack = d.transStack[:n-1] - - case ops.TypePushOpacity: - opacity := ops.DecodeOpacity(encOp.Data) - parent := -1 - depth := len(d.opacityStack) - if depth > 0 { - parent = d.opacityStack[depth-1] - } - lidx := len(d.layers) - d.layers = append(d.layers, opacityLayer{ - opacity: opacity, - parent: parent, - depth: depth, - opStart: len(d.imageOps), - }) - d.opacityStack = append(d.opacityStack, lidx) - case ops.TypePopOpacity: - n := len(d.opacityStack) - idx := d.opacityStack[n-1] - d.layers[idx].opEnd = len(d.imageOps) - d.opacityStack = d.opacityStack[:n-1] - - case ops.TypeStroke: - quads.key.strokeWidth = decodeStrokeOp(encOp.Data) - - case ops.TypePath: - encOp, ok = r.Decode() - if !ok { - break loop - } - quads.aux = encOp.Data[ops.TypeAuxLen:] - quads.key.Key = encOp.Key - - case ops.TypeClip: - var op ops.ClipOp - op.Decode(encOp.Data) - quads.key.outline = op.Outline - bounds := op.Bounds - trans, off := transformOffset(state.t) - if len(quads.aux) > 0 { - // There is a clipping path, build the gpu data and update the - // cache key such that it will be equal only if the transform is the - // same also. Use cached data if we have it. - quads.key = quads.key.SetTransform(trans) - if v, ok := d.pathCache.get(quads.key); ok { - // Since the GPU data exists in the cache aux will not be used. - // Why is this not used for the offset shapes? - bounds = v.bounds - } else { - newPathData, newBounds := d.buildVerts( - quads.aux, trans, quads.key.outline, quads.key.strokeWidth, - ) - quads.aux = newPathData - bounds = newBounds.Round() - // add it to the cache, without GPU data, so the transform can be - // reused. - d.pathCache.put(quads.key, opCacheValue{bounds: bounds}) - } - } else { - quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans) - quads.key = opKey{Key: encOp.Key} - quads.key = quads.key.SetTransform(trans) - } - d.addClipPath(&state, quads.aux, quads.key, bounds, off) - quads = quadsOp{} - case ops.TypePopClip: - state.cpath = state.cpath.parent - - case ops.TypeColor: - state.matType = materialColor - state.color = decodeColorOp(encOp.Data) - case ops.TypeLinearGradient: - state.matType = materialLinearGradient - op := decodeLinearGradientOp(encOp.Data) - state.stop1 = op.stop1 - state.stop2 = op.stop2 - state.color1 = op.color1 - state.color2 = op.color2 - case ops.TypeImage: - state.matType = materialTexture - state.image = decodeImageOp(encOp.Data, encOp.Refs) - case ops.TypePaint: - // Transform (if needed) the painting rectangle and if so generate a clip path, - // for those cases also compute a partialTrans that maps texture coordinates between - // the new bounding rectangle and the transformed original paint rectangle. - t, off := transformOffset(state.t) - // Fill the clip area, unless the material is a (bounded) image. - // TODO: Find a tighter bound. - inf := int(1e6) - dst := image.Rect(-inf, -inf, inf, inf) - if state.matType == materialTexture { - sz := state.image.src.Rect.Size() - dst = image.Rectangle{Max: sz} - } - clipData, bnd, partialTrans := d.boundsForTransformedRect(dst, t) - bounds := viewport.Intersect(bnd.Add(off)) - if state.cpath != nil { - bounds = state.cpath.intersect.Intersect(bounds) - } - if bounds.Empty() { - continue - } - - if clipData != nil { - // The paint operation is sheared or rotated, add a clip path representing - // this transformed rectangle. - k := opKey{Key: encOp.Key} - k = k.SetTransform(t) - d.addClipPath(&state, clipData, k, bnd, off) - } - - mat := state.materialFor(bnd, off, partialTrans, bounds) - - rect := state.cpath == nil || state.cpath.rect - if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) && len(d.opacityStack) == 0 { - // The image is a uniform opaque color and takes up the whole screen. - // Scrap images up to and including this image and set clear color. - d.imageOps = d.imageOps[:0] - d.clearColor = mat.color.Opaque() - d.clear = true - continue - } - img := imageOp{ - path: state.cpath, - clip: bounds, - material: mat, - } - if n := len(d.opacityStack); n > 0 { - idx := d.opacityStack[n-1] - lb := d.layers[idx].clip - if lb.Empty() { - d.layers[idx].clip = img.clip - } else { - d.layers[idx].clip = lb.Union(img.clip) - } - } - - d.imageOps = append(d.imageOps, img) - if clipData != nil { - // we added a clip path that should not remain - state.cpath = state.cpath.parent - } - case ops.TypeSave: - id := ops.DecodeSave(encOp.Data) - d.save(id, state.t) - case ops.TypeLoad: - reset() - id := ops.DecodeLoad(encOp.Data) - state.t = d.states[id] - } - } -} - -func expandPathOp(p *pathOp, clip image.Rectangle) { - for p != nil { - pclip := p.clip - if !pclip.Empty() { - clip = clip.Union(pclip) - } - p.clip = clip - p = p.parent - } -} - -func (d *drawState) materialFor(rect image.Rectangle, off image.Point, partTrans f32.Affine2D, clip image.Rectangle) material { - m := material{ - opacity: 1., - uvTrans: f32.AffineId(), - } - switch d.matType { - case materialColor: - m.material = materialColor - m.color = f32color.LinearFromSRGB(d.color) - m.opaque = m.color.A == 1.0 - case materialLinearGradient: - m.material = materialLinearGradient - - m.color1 = f32color.LinearFromSRGB(d.color1) - m.color2 = f32color.LinearFromSRGB(d.color2) - m.opaque = m.color1.A == 1.0 && m.color2.A == 1.0 - - m.uvTrans = partTrans.Mul(gradientSpaceTransform(clip, off, d.stop1, d.stop2)) - case materialTexture: - m.material = materialTexture - dr := rect.Add(off) - sz := d.image.src.Bounds().Size() - sr := f32.Rectangle{ - Max: f32.Point{ - X: float32(sz.X), - Y: float32(sz.Y), - }, - } - dx := float32(dr.Dx()) - sdx := sr.Dx() - sr.Min.X += float32(clip.Min.X-dr.Min.X) * sdx / dx - sr.Max.X -= float32(dr.Max.X-clip.Max.X) * sdx / dx - dy := float32(dr.Dy()) - sdy := sr.Dy() - sr.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy - sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy - uvScale, uvOffset := texSpaceTransform(sr, sz) - m.uvTrans = partTrans.Mul(f32.AffineId().Scale(f32.Point{}, uvScale).Offset(uvOffset)) - m.data = d.image - } - return m -} - -func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) { - for i := range ops { - img := &ops[i] - m := img.material - if m.material == materialTexture { - img.material.tex = r.texHandle(cache, m.data) - } - } -} - -func (r *renderer) prepareDrawOps(ops []imageOp) { - for _, img := range ops { - m := img.material - switch m.material { - case materialTexture: - r.ctx.PrepareTexture(m.tex) - } - - var fbo FBO - switch img.clipType { - case clipTypeNone: - continue - case clipTypePath: - fbo = r.pather.stenciler.cover(img.place.Idx) - case clipTypeIntersection: - fbo = r.pather.stenciler.intersections.fbos[img.place.Idx] - } - r.ctx.PrepareTexture(fbo.tex) - } -} - -func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp) { - var coverTex driver.Texture - for i := 0; i < len(ops); i++ { - img := ops[i] - i += img.layerOps - m := img.material - switch m.material { - case materialTexture: - r.ctx.BindTexture(0, m.tex) - } - drc := img.clip.Add(opOff) - - scale, off := clipSpaceTransform(drc, viewport) - var fbo FBO - fboIdx := 0 - if isFBO { - fboIdx = 1 - } - switch img.clipType { - case clipTypeNone: - p := r.blitter.pipelines[fboIdx][m.material] - r.ctx.BindPipeline(p.pipeline) - r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0) - r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans) - continue - case clipTypePath: - fbo = r.pather.stenciler.cover(img.place.Idx) - case clipTypeIntersection: - fbo = r.pather.stenciler.intersections.fbos[img.place.Idx] - } - if coverTex != fbo.tex { - coverTex = fbo.tex - r.ctx.BindTexture(1, coverTex) - } - uv := image.Rectangle{ - Min: img.place.Pos, - Max: img.place.Pos.Add(drc.Size()), - } - coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size) - p := r.pather.coverer.pipelines[fboIdx][m.material] - r.ctx.BindPipeline(p.pipeline) - r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0) - r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff) - } -} - -func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) { - fboIdx := 0 - if fbo { - fboIdx = 1 - } - p := b.pipelines[fboIdx][mat] - b.ctx.BindPipeline(p.pipeline) - var uniforms *blitUniforms - switch mat { - case materialColor: - b.colUniforms.color = col - uniforms = &b.colUniforms.blitUniforms - case materialTexture: - t1, t2, t3, t4, t5, t6 := uvTrans.Elems() - uniforms = &b.texUniforms.blitUniforms - uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} - uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} - case materialLinearGradient: - b.linearGradientUniforms.color1 = col1 - b.linearGradientUniforms.color2 = col2 - - t1, t2, t3, t4, t5, t6 := uvTrans.Elems() - uniforms = &b.linearGradientUniforms.blitUniforms - uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} - uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} - } - uniforms.fbo = 0 - if fbo { - uniforms.fbo = 1 - } - uniforms.opacity = opacity - uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} - p.UploadUniforms(b.ctx) - b.ctx.DrawArrays(0, 4) -} - -// newUniformBuffer creates a new GPU uniform buffer backed by the -// structure uniformBlock points to. -func newUniformBuffer(b driver.Device, uniformBlock any) *uniformBuffer { - ref := reflect.ValueOf(uniformBlock) - // Determine the size of the uniforms structure, *uniforms. - size := ref.Elem().Type().Size() - // Map the uniforms structure as a byte slice. - ptr := unsafe.Slice((*byte)(unsafe.Pointer(ref.Pointer())), size) - ubuf, err := b.NewBuffer(driver.BufferBindingUniforms, len(ptr)) - if err != nil { - panic(err) - } - return &uniformBuffer{buf: ubuf, ptr: ptr} -} - -func (u *uniformBuffer) Upload() { - u.buf.Upload(u.ptr) -} - -func (u *uniformBuffer) Release() { - u.buf.Release() - u.buf = nil -} - -func (p *pipeline) UploadUniforms(ctx driver.Device) { - if p.uniforms != nil { - p.uniforms.Upload() - ctx.BindUniforms(p.uniforms.buf) - } -} - -func (p *pipeline) Release() { - p.pipeline.Release() - if p.uniforms != nil { - p.uniforms.Release() - } - *p = pipeline{} -} - -// texSpaceTransform return the scale and offset that transforms the given subimage -// into quad texture coordinates. -func texSpaceTransform(r f32.Rectangle, bounds image.Point) (f32.Point, f32.Point) { - size := f32.Point{X: float32(bounds.X), Y: float32(bounds.Y)} - scale := f32.Point{X: r.Dx() / size.X, Y: r.Dy() / size.Y} - offset := f32.Point{X: r.Min.X / size.X, Y: r.Min.Y / size.Y} - return scale, offset -} - -// gradientSpaceTransform transforms stop1 and stop2 to [(0,0), (1,1)]. -func gradientSpaceTransform(clip image.Rectangle, off image.Point, stop1, stop2 f32.Point) f32.Affine2D { - d := stop2.Sub(stop1) - l := float32(math.Sqrt(float64(d.X*d.X + d.Y*d.Y))) - a := float32(math.Atan2(float64(-d.Y), float64(d.X))) - - // TODO: optimize - zp := f32.Point{} - return f32.AffineId(). - Scale(zp, layout.FPt(clip.Size())). // scale to pixel space - Offset(zp.Sub(f32.FPt(off)).Add(layout.FPt(clip.Min))). // offset to clip space - Offset(zp.Sub(stop1)). // offset to first stop point - Rotate(zp, a). // rotate to align gradient - Scale(zp, f32.Pt(1/l, 1/l)) // scale gradient to right size -} - -// clipSpaceTransform returns the scale and offset that transforms the given -// rectangle from a viewport into GPU driver device coordinates. -func clipSpaceTransform(r image.Rectangle, viewport image.Point) (f32.Point, f32.Point) { - // First, transform UI coordinates to device coordinates: - // - // [(-1, -1) (+1, -1)] - // [(-1, +1) (+1, +1)] - // - x, y := float32(r.Min.X), float32(r.Min.Y) - w, h := float32(r.Dx()), float32(r.Dy()) - vx, vy := 2/float32(viewport.X), 2/float32(viewport.Y) - x = x*vx - 1 - y = y*vy - 1 - w *= vx - h *= vy - - // Then, compute the transformation from the fullscreen quad to - // the rectangle at (x, y) and dimensions (w, h). - scale := f32.Point{X: w * .5, Y: h * .5} - offset := f32.Point{X: x + w*.5, Y: y + h*.5} - - return scale, offset -} - -// Fill in maximal Y coordinates of the NW and NE corners. -func fillMaxY(verts []byte) { - contour := 0 - bo := binary.LittleEndian - for len(verts) > 0 { - maxy := float32(math.Inf(-1)) - i := 0 - for ; i+vertStride*4 <= len(verts); i += vertStride * 4 { - vert := verts[i : i+vertStride] - // MaxY contains the integer contour index. - pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):])) - if contour != pathContour { - contour = pathContour - break - } - fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):])) - ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):])) - toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):])) - if fromy > maxy { - maxy = fromy - } - if ctrly > maxy { - maxy = ctrly - } - if toy > maxy { - maxy = toy - } - } - fillContourMaxY(maxy, verts[:i]) - verts = verts[i:] - } -} - -func fillContourMaxY(maxy float32, verts []byte) { - bo := binary.LittleEndian - for i := 0; i < len(verts); i += vertStride { - off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY)) - bo.PutUint32(verts[i+off:], math.Float32bits(maxy)) - } -} - -func (d *drawOps) writeVertCache(n int) []byte { - d.vertCache = append(d.vertCache, make([]byte, n)...) - return d.vertCache[len(d.vertCache)-n:] -} - -// transform, split paths as needed, calculate maxY, bounds and create GPU vertices. -func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, strWidth float32) (verts []byte, bounds f32.Rectangle) { - inf := float32(math.Inf(+1)) - d.qs.bounds = f32.Rectangle{ - Min: f32.Point{X: inf, Y: inf}, - Max: f32.Point{X: -inf, Y: -inf}, - } - d.qs.d = d - startLength := len(d.vertCache) - - switch { - case strWidth > 0: - // Stroke path. - ss := stroke.StrokeStyle{ - Width: strWidth, - } - quads := stroke.StrokePathCommands(ss, pathData) - for _, quad := range quads { - d.qs.contour = quad.Contour - quad.Quad = quad.Quad.Transform(tr) - - d.qs.splitAndEncode(quad.Quad) - } - - case outline: - decodeToOutlineQuads(&d.qs, tr, pathData) - } - - fillMaxY(d.vertCache[startLength:]) - return d.vertCache[startLength:], d.qs.bounds -} - -// decodeOutlineQuads decodes scene commands, splits them into quadratic béziers -// as needed and feeds them to the supplied splitter. -func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) { - for len(pathData) >= scene.CommandSize+4 { - qs.contour = binary.LittleEndian.Uint32(pathData) - cmd := ops.DecodeCommand(pathData[4:]) - switch cmd.Op() { - case scene.OpLine: - var q stroke.QuadSegment - q.From, q.To = scene.DecodeLine(cmd) - q.Ctrl = q.From.Add(q.To).Mul(.5) - q = q.Transform(tr) - qs.splitAndEncode(q) - case scene.OpGap: - var q stroke.QuadSegment - q.From, q.To = scene.DecodeGap(cmd) - q.Ctrl = q.From.Add(q.To).Mul(.5) - q = q.Transform(tr) - qs.splitAndEncode(q) - case scene.OpQuad: - var q stroke.QuadSegment - q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) - q = q.Transform(tr) - qs.splitAndEncode(q) - case scene.OpCubic: - from, ctrl0, ctrl1, to := scene.DecodeCubic(cmd) - qs.scratch = stroke.SplitCubic(from, ctrl0, ctrl1, to, qs.scratch[:0]) - for _, q := range qs.scratch { - q = q.Transform(tr) - qs.splitAndEncode(q) - } - default: - panic("unsupported scene command") - } - pathData = pathData[scene.CommandSize+4:] - } -} - -// create GPU vertices for transformed r, find the bounds and establish texture transform. -func (d *drawOps) boundsForTransformedRect(r image.Rectangle, tr f32.Affine2D) (aux []byte, bnd image.Rectangle, ptr f32.Affine2D) { - ptr = f32.AffineId() - if tr == f32.AffineId() { - // fast-path to allow blitting of pure rectangles. - bnd = r - return - } - - // transform all corners, find new bounds - corners := [4]f32.Point{ - tr.Transform(f32.FPt(r.Min)), tr.Transform(f32.Pt(float32(r.Max.X), float32(r.Min.Y))), - tr.Transform(f32.FPt(r.Max)), tr.Transform(f32.Pt(float32(r.Min.X), float32(r.Max.Y))), - } - fBounds := f32.Rectangle{ - Min: f32.Pt(math.MaxFloat32, math.MaxFloat32), - Max: f32.Pt(-math.MaxFloat32, -math.MaxFloat32), - } - for _, c := range corners { - if c.X < fBounds.Min.X { - fBounds.Min.X = c.X - } - if c.Y < fBounds.Min.Y { - fBounds.Min.Y = c.Y - } - if c.X > fBounds.Max.X { - fBounds.Max.X = c.X - } - if c.Y > fBounds.Max.Y { - fBounds.Max.Y = c.Y - } - } - bnd = fBounds.Round() - - // build the GPU vertices - l := len(d.vertCache) - d.vertCache = append(d.vertCache, make([]byte, vertStride*4*4)...) - aux = d.vertCache[l:] - encodeQuadTo(aux, 0, corners[0], corners[0].Add(corners[1]).Mul(0.5), corners[1]) - encodeQuadTo(aux[vertStride*4:], 0, corners[1], corners[1].Add(corners[2]).Mul(0.5), corners[2]) - encodeQuadTo(aux[vertStride*4*2:], 0, corners[2], corners[2].Add(corners[3]).Mul(0.5), corners[3]) - encodeQuadTo(aux[vertStride*4*3:], 0, corners[3], corners[3].Add(corners[0]).Mul(0.5), corners[0]) - fillMaxY(aux) - - // establish the transform mapping from bounds rectangle to transformed corners - var P1, P2, P3 f32.Point - P1.X = (corners[1].X - fBounds.Min.X) / (fBounds.Max.X - fBounds.Min.X) - P1.Y = (corners[1].Y - fBounds.Min.Y) / (fBounds.Max.Y - fBounds.Min.Y) - P2.X = (corners[2].X - fBounds.Min.X) / (fBounds.Max.X - fBounds.Min.X) - P2.Y = (corners[2].Y - fBounds.Min.Y) / (fBounds.Max.Y - fBounds.Min.Y) - P3.X = (corners[3].X - fBounds.Min.X) / (fBounds.Max.X - fBounds.Min.X) - P3.Y = (corners[3].Y - fBounds.Min.Y) / (fBounds.Max.Y - fBounds.Min.Y) - sx, sy := P2.X-P3.X, P2.Y-P3.Y - ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, P1.Y-sy).Invert() - - return aux, bnd, ptr -} - -// transformOffset a transform into two parts, one which is pure integer offset -// and the other representing the scaling, shearing and rotation and fractional -// offset. -func transformOffset(t f32.Affine2D) (f32.Affine2D, image.Point) { - sx, hx, ox, hy, sy, oy := t.Elems() - iox, fox := math.Modf(float64(ox)) - ioy, foy := math.Modf(float64(oy)) - ft := f32.NewAffine2D(sx, hx, float32(fox), hy, sy, float32(foy)) - ip := image.Pt(int(iox), int(ioy)) - return ft, ip -} - -func newShaders(ctx driver.Device, vsrc, fsrc shader.Sources) (vert driver.VertexShader, frag driver.FragmentShader, err error) { - vert, err = ctx.NewVertexShader(vsrc) - if err != nil { - return - } - frag, err = ctx.NewFragmentShader(fsrc) - if err != nil { - vert.Release() - } - return -} diff --git a/gio/gpu/headless/driver_test.go b/gio/gpu/headless/driver_test.go deleted file mode 100644 index 019e9ac..0000000 --- a/gio/gpu/headless/driver_test.go +++ /dev/null @@ -1,208 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package headless - -import ( - "bytes" - "flag" - "image" - "image/color" - "image/png" - "os" - "runtime" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" - "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/shader" - "github.com/p9c/p9/pkg/gel/gio/shader/gio" -) - -var dumpImages = flag.Bool("saveimages", false, "save test images") - -var ( - clearCol = color.NRGBA{A: 0xff, R: 0xde, G: 0xad, B: 0xbe} - clearColExpect = f32color.NRGBAToRGBA(clearCol) -) - -func TestFramebufferClear(t *testing.T) { - b := newDriver(t) - sz := image.Point{X: 800, Y: 600} - fbo := newFBO(t, b, sz) - d := driver.LoadDesc{ - // ClearColor accepts linear RGBA colors, while 8-bit colors - // are in the sRGB color space. - ClearColor: f32color.LinearFromSRGB(clearCol), - Action: driver.LoadActionClear, - } - b.BeginRenderPass(fbo, d) - b.EndRenderPass() - img := screenshot(t, b, fbo, sz) - if got := img.RGBAAt(0, 0); got != clearColExpect { - t.Errorf("got color %v, expected %v", got, clearColExpect) - } -} - -func TestInputShader(t *testing.T) { - b := newDriver(t) - sz := image.Point{X: 800, Y: 600} - vsh, fsh, err := newShaders(b, gio.Shader_input_vert, gio.Shader_simple_frag) - if err != nil { - t.Fatal(err) - } - defer vsh.Release() - defer fsh.Release() - layout := driver.VertexLayout{ - Inputs: []driver.InputDesc{ - { - Type: shader.DataTypeFloat, - Size: 4, - Offset: 0, - }, - }, - Stride: 4 * 4, - } - fbo := newFBO(t, b, sz) - pipe, err := b.NewPipeline(driver.PipelineDesc{ - VertexShader: vsh, - FragmentShader: fsh, - VertexLayout: layout, - PixelFormat: driver.TextureFormatSRGBA, - Topology: driver.TopologyTriangles, - }) - if err != nil { - t.Fatal(err) - } - defer pipe.Release() - buf, err := b.NewImmutableBuffer(driver.BufferBindingVertices, - byteslice.Slice([]float32{ - 0, -.5, .5, 1, - -.5, +.5, .5, 1, - .5, +.5, .5, 1, - }), - ) - if err != nil { - t.Fatal(err) - } - defer buf.Release() - d := driver.LoadDesc{ - ClearColor: f32color.LinearFromSRGB(clearCol), - Action: driver.LoadActionClear, - } - b.BeginRenderPass(fbo, d) - b.Viewport(0, 0, sz.X, sz.Y) - b.BindPipeline(pipe) - b.BindVertexBuffer(buf, 0) - b.DrawArrays(0, 3) - b.EndRenderPass() - img := screenshot(t, b, fbo, sz) - if got := img.RGBAAt(0, 0); got != clearColExpect { - t.Errorf("got color %v, expected %v", got, clearColExpect) - } - cx, cy := 300, 400 - shaderCol := f32color.RGBA{R: .25, G: .55, B: .75, A: 1.0} - if got, exp := img.RGBAAt(cx, cy), shaderCol.SRGB(); got != f32color.NRGBAToRGBA(exp) { - t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(exp)) - } -} - -func newShaders(ctx driver.Device, vsrc, fsrc shader.Sources) (vert driver.VertexShader, frag driver.FragmentShader, err error) { - vert, err = ctx.NewVertexShader(vsrc) - if err != nil { - return - } - frag, err = ctx.NewFragmentShader(fsrc) - if err != nil { - vert.Release() - } - return -} - -func TestFramebuffers(t *testing.T) { - b := newDriver(t) - sz := image.Point{X: 800, Y: 600} - var ( - col1 = color.NRGBA{R: 0xac, G: 0xbd, B: 0xef, A: 0xde} - col2 = color.NRGBA{R: 0xfe, G: 0xbb, B: 0xbe, A: 0xca} - ) - fbo1 := newFBO(t, b, sz) - fbo2 := newFBO(t, b, sz) - fcol1, fcol2 := f32color.LinearFromSRGB(col1), f32color.LinearFromSRGB(col2) - d := driver.LoadDesc{Action: driver.LoadActionClear} - d.ClearColor = fcol1 - b.BeginRenderPass(fbo1, d) - b.EndRenderPass() - d.ClearColor = fcol2 - b.BeginRenderPass(fbo2, d) - b.EndRenderPass() - img := screenshot(t, b, fbo1, sz) - if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col1) { - t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col1)) - } - img = screenshot(t, b, fbo2, sz) - if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col2) { - t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col2)) - } -} - -func newFBO(t *testing.T, b driver.Device, size image.Point) driver.Texture { - fboTex, err := b.NewTexture( - driver.TextureFormatSRGBA, - size.X, size.Y, - driver.FilterNearest, driver.FilterNearest, - driver.BufferBindingFramebuffer, - ) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { - fboTex.Release() - }) - return fboTex -} - -func newDriver(t *testing.T) driver.Device { - ctx, err := newContext() - if err != nil { - t.Skipf("no context available: %v", err) - } - if err := ctx.MakeCurrent(); err != nil { - t.Fatal(err) - } - b, err := driver.NewDevice(ctx.API()) - if err != nil { - t.Fatal(err) - } - b.BeginFrame(nil, true, image.Pt(1, 1)) - t.Cleanup(func() { - b.EndFrame() - b.Release() - ctx.ReleaseCurrent() - runtime.UnlockOSThread() - ctx.Release() - }) - return b -} - -func screenshot(t *testing.T, d driver.Device, fbo driver.Texture, size image.Point) *image.RGBA { - img := image.NewRGBA(image.Rectangle{Max: size}) - err := driver.DownloadImage(d, fbo, img) - if err != nil { - t.Fatal(err) - } - if *dumpImages { - if err := saveImage(t.Name()+".png", img); err != nil { - t.Error(err) - } - } - return img -} - -func saveImage(file string, img image.Image) error { - var buf bytes.Buffer - if err := png.Encode(&buf, img); err != nil { - return err - } - return os.WriteFile(file, buf.Bytes(), 0o666) -} diff --git a/gio/gpu/headless/headless.go b/gio/gpu/headless/headless.go deleted file mode 100644 index 3722122..0000000 --- a/gio/gpu/headless/headless.go +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package headless implements headless windows for rendering -// an operation list to an image. -package headless - -import ( - "errors" - "image" - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Window is a headless window. -type Window struct { - size image.Point - ctx context - dev driver.Device - gpu gpu.GPU - fboTex driver.Texture -} - -type context interface { - API() gpu.API - MakeCurrent() error - ReleaseCurrent() - Release() -} - -var ( - newContextPrimary func() (context, error) - newContextFallback func() (context, error) -) - -func newContext() (context, error) { - funcs := []func() (context, error){newContextPrimary, newContextFallback} - var firstErr error - for _, f := range funcs { - if f == nil { - continue - } - c, err := f() - if err != nil { - if firstErr == nil { - firstErr = err - } - continue - } - return c, nil - } - if firstErr != nil { - return nil, firstErr - } - return nil, errors.New("headless: no available GPU backends") -} - -// NewWindow creates a new headless window. -func NewWindow(width, height int) (*Window, error) { - ctx, err := newContext() - if err != nil { - return nil, err - } - w := &Window{ - size: image.Point{X: width, Y: height}, - ctx: ctx, - } - err = contextDo(ctx, func() error { - dev, err := driver.NewDevice(ctx.API()) - if err != nil { - return err - } - fboTex, err := dev.NewTexture( - driver.TextureFormatSRGBA, - width, height, - driver.FilterNearest, driver.FilterNearest, - driver.BufferBindingFramebuffer, - ) - if err != nil { - dev.Release() - return err - } - // Note that the gpu takes ownership of dev. - gp, err := gpu.NewWithDevice(dev) - if err != nil { - fboTex.Release() - return err - } - w.fboTex = fboTex - w.gpu = gp - w.dev = dev - return err - }) - if err != nil { - ctx.Release() - return nil, err - } - return w, nil -} - -// Release resources associated with the window. -func (w *Window) Release() { - contextDo(w.ctx, func() error { - if w.fboTex != nil { - w.fboTex.Release() - w.fboTex = nil - } - if w.gpu != nil { - w.gpu.Release() - w.gpu = nil - } - // w.dev is owned and freed by w.gpu. - w.dev = nil - return nil - }) - if w.ctx != nil { - w.ctx.Release() - w.ctx = nil - } -} - -// Size returns the window size. -func (w *Window) Size() image.Point { - return w.size -} - -// Frame replaces the window content and state with the -// operation list. -func (w *Window) Frame(frame *op.Ops) error { - return contextDo(w.ctx, func() error { - w.gpu.Clear(color.NRGBA{}) - return w.gpu.Frame(frame, w.fboTex, w.size) - }) -} - -// Screenshot transfers the Window content at origin img.Rect.Min to img. -func (w *Window) Screenshot(img *image.RGBA) error { - return contextDo(w.ctx, func() error { - return driver.DownloadImage(w.dev, w.fboTex, img) - }) -} - -func contextDo(ctx context, f func() error) error { - errCh := make(chan error) - go func() { - if err := ctx.MakeCurrent(); err != nil { - errCh <- err - return - } - err := f() - ctx.ReleaseCurrent() - errCh <- err - }() - return <-errCh -} diff --git a/gio/gpu/headless/headless_darwin.go b/gio/gpu/headless/headless_darwin.go deleted file mode 100644 index 0dd0d43..0000000 --- a/gio/gpu/headless/headless_darwin.go +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package headless - -import ( - "errors" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - _ "github.com/p9c/p9/pkg/gel/gio/internal/cocoainit" -) - -/* -#cgo CFLAGS: -Werror -Wno-deprecated-declarations -fobjc-arc -x objective-c -#cgo LDFLAGS: -framework CoreGraphics -framework Metal -framework Foundation - -#import - -static CFTypeRef createDevice(void) { - @autoreleasepool { - id dev = MTLCreateSystemDefaultDevice(); - return CFBridgingRetain(dev); - } -} - -static CFTypeRef newCommandQueue(CFTypeRef devRef) { - @autoreleasepool { - id dev = (__bridge id)devRef; - return CFBridgingRetain([dev newCommandQueue]); - } -} -*/ -import "C" - -type mtlContext struct { - dev C.CFTypeRef - queue C.CFTypeRef -} - -func init() { - newContextPrimary = func() (context, error) { - dev := C.createDevice() - if dev == 0 { - return nil, errors.New("headless: failed to create Metal device") - } - queue := C.newCommandQueue(dev) - if queue == 0 { - C.CFRelease(dev) - return nil, errors.New("headless: failed to create MTLQueue") - } - return &mtlContext{dev: dev, queue: queue}, nil - } -} - -func (c *mtlContext) API() gpu.API { - return gpu.Metal{ - Device: uintptr(c.dev), - Queue: uintptr(c.queue), - PixelFormat: int(C.MTLPixelFormatRGBA8Unorm_sRGB), - } -} - -func (c *mtlContext) MakeCurrent() error { - return nil -} - -func (c *mtlContext) ReleaseCurrent() {} - -func (d *mtlContext) Release() { - C.CFRelease(d.dev) - C.CFRelease(d.queue) - *d = mtlContext{} -} diff --git a/gio/gpu/headless/headless_egl.go b/gio/gpu/headless/headless_egl.go deleted file mode 100644 index e30d673..0000000 --- a/gio/gpu/headless/headless_egl.go +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build linux || freebsd || openbsd -// +build linux freebsd openbsd - -package headless - -import ( - "github.com/p9c/p9/pkg/gel/gio/internal/egl" -) - -func init() { - newContextPrimary = func() (context, error) { - return egl.NewContext(egl.EGL_DEFAULT_DISPLAY) - } -} diff --git a/gio/gpu/headless/headless_js.go b/gio/gpu/headless/headless_js.go deleted file mode 100644 index 3c16723..0000000 --- a/gio/gpu/headless/headless_js.go +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package headless - -import ( - "errors" - "syscall/js" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/gl" -) - -type jsContext struct { - ctx js.Value -} - -func init() { - newContextPrimary = func() (context, error) { - doc := js.Global().Get("document") - cnv := doc.Call("createElement", "canvas") - ctx := cnv.Call("getContext", "webgl2") - if ctx.IsNull() { - ctx = cnv.Call("getContext", "webgl") - } - if ctx.IsNull() { - return nil, errors.New("headless: webgl is not supported") - } - c := &jsContext{ - ctx: ctx, - } - return c, nil - } -} - -func (c *jsContext) API() gpu.API { - return gpu.OpenGL{Context: gl.Context(c.ctx)} -} - -func (c *jsContext) Release() { -} - -func (c *jsContext) ReleaseCurrent() { -} - -func (c *jsContext) MakeCurrent() error { - return nil -} diff --git a/gio/gpu/headless/headless_test.go b/gio/gpu/headless/headless_test.go deleted file mode 100644 index 886fa79..0000000 --- a/gio/gpu/headless/headless_test.go +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package headless - -import ( - "image" - "image/color" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -func TestHeadless(t *testing.T) { - w, release := newTestWindow(t) - defer release() - - sz := w.size - col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe} - var ops op.Ops - paint.ColorOp{Color: col}.Add(&ops) - // Paint only part of the screen to avoid the glClear optimization. - paint.FillShape(&ops, col, clip.Rect(image.Rect(0, 0, sz.X-100, sz.Y-100)).Op()) - if err := w.Frame(&ops); err != nil { - t.Fatal(err) - } - - img := image.NewRGBA(image.Rectangle{Max: w.Size()}) - err := w.Screenshot(img) - if err != nil { - t.Fatal(err) - } - if isz := img.Bounds().Size(); isz != sz { - t.Errorf("got %v screenshot, expected %v", isz, sz) - } - if got := img.RGBAAt(0, 0); got != f32color.NRGBAToRGBA(col) { - t.Errorf("got color %v, expected %v", got, f32color.NRGBAToRGBA(col)) - } -} - -func TestClipping(t *testing.T) { - w, release := newTestWindow(t) - defer release() - - col := color.NRGBA{A: 0xff, R: 0xca, G: 0xfe} - col2 := color.NRGBA{A: 0xff, R: 0x00, G: 0xfe} - var ops op.Ops - paint.ColorOp{Color: col}.Add(&ops) - clip.RRect{ - Rect: image.Rect(50, 50, 250, 250), - SE: 75, - }.Push(&ops) - paint.PaintOp{}.Add(&ops) - paint.ColorOp{Color: col2}.Add(&ops) - clip.RRect{ - Rect: image.Rect(100, 100, 350, 350), - NW: 75, - }.Push(&ops) - paint.PaintOp{}.Add(&ops) - if err := w.Frame(&ops); err != nil { - t.Fatal(err) - } - - img := image.NewRGBA(image.Rectangle{Max: w.Size()}) - err := w.Screenshot(img) - if err != nil { - t.Fatal(err) - } - if *dumpImages { - if err := saveImage("clip.png", img); err != nil { - t.Fatal(err) - } - } - var bg color.NRGBA - tests := []struct { - x, y int - color color.NRGBA - }{ - {120, 120, col}, - {130, 130, col2}, - {210, 210, col2}, - {230, 230, bg}, - } - for _, test := range tests { - if got := img.RGBAAt(test.x, test.y); got != f32color.NRGBAToRGBA(test.color) { - t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got, f32color.NRGBAToRGBA(test.color)) - } - } -} - -func TestDepth(t *testing.T) { - w, release := newTestWindow(t) - defer release() - var ops op.Ops - - blue := color.NRGBA{B: 0xFF, A: 0xFF} - paint.FillShape(&ops, blue, clip.Rect(image.Rect(0, 0, 50, 100)).Op()) - red := color.NRGBA{R: 0xFF, A: 0xFF} - paint.FillShape(&ops, red, clip.Rect(image.Rect(0, 0, 100, 50)).Op()) - if err := w.Frame(&ops); err != nil { - t.Fatal(err) - } - - img := image.NewRGBA(image.Rectangle{Max: w.Size()}) - err := w.Screenshot(img) - if err != nil { - t.Fatal(err) - } - if *dumpImages { - if err := saveImage("depth.png", img); err != nil { - t.Fatal(err) - } - } - tests := []struct { - x, y int - color color.NRGBA - }{ - {25, 25, red}, - {75, 25, red}, - {25, 75, blue}, - } - for _, test := range tests { - if got := img.RGBAAt(test.x, test.y); got != f32color.NRGBAToRGBA(test.color) { - t.Errorf("(%d,%d): got color %v, expected %v", test.x, test.y, got, f32color.NRGBAToRGBA(test.color)) - } - } -} - -func TestNoOps(t *testing.T) { - w, release := newTestWindow(t) - defer release() - if err := w.Frame(nil); err != nil { - t.Error(err) - } -} - -func newTestWindow(t *testing.T) (*Window, func()) { - t.Helper() - sz := image.Point{X: 800, Y: 600} - w, err := NewWindow(sz.X, sz.Y) - if err != nil { - t.Skipf("headless windows not supported: %v", err) - } - return w, func() { - w.Release() - } -} diff --git a/gio/gpu/headless/headless_vulkan.go b/gio/gpu/headless/headless_vulkan.go deleted file mode 100644 index 498c629..0000000 --- a/gio/gpu/headless/headless_vulkan.go +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build (linux || freebsd) && !novulkan -// +build linux freebsd -// +build !novulkan - -package headless - -import ( - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/vk" -) - -type vkContext struct { - physDev vk.PhysicalDevice - inst vk.Instance - dev vk.Device - queueFam int -} - -func init() { - newContextFallback = newVulkanContext -} - -func newVulkanContext() (context, error) { - inst, err := vk.CreateInstance() - if err != nil { - return nil, err - } - physDev, qFam, err := vk.ChoosePhysicalDevice(inst, 0) - if err != nil { - vk.DestroyInstance(inst) - return nil, err - } - dev, err := vk.CreateDeviceAndQueue(physDev, qFam) - if err != nil { - vk.DestroyInstance(inst) - return nil, err - } - ctx := &vkContext{ - physDev: physDev, - inst: inst, - dev: dev, - queueFam: qFam, - } - return ctx, nil -} - -func (c *vkContext) API() gpu.API { - return gpu.Vulkan{ - PhysDevice: unsafe.Pointer(c.physDev), - Device: unsafe.Pointer(c.dev), - Format: int(vk.FORMAT_R8G8B8A8_SRGB), - QueueFamily: c.queueFam, - QueueIndex: 0, - } -} - -func (c *vkContext) MakeCurrent() error { - return nil -} - -func (c *vkContext) ReleaseCurrent() { -} - -func (c *vkContext) Release() { - vk.DeviceWaitIdle(c.dev) - - vk.DestroyDevice(c.dev) - vk.DestroyInstance(c.inst) - *c = vkContext{} -} diff --git a/gio/gpu/headless/headless_windows.go b/gio/gpu/headless/headless_windows.go deleted file mode 100644 index f379432..0000000 --- a/gio/gpu/headless/headless_windows.go +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package headless - -import ( - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu" - "github.com/p9c/p9/pkg/gel/gio/internal/d3d11" -) - -type d3d11Context struct { - dev *d3d11.Device -} - -func init() { - newContextPrimary = func() (context, error) { - dev, ctx, _, err := d3d11.CreateDevice( - d3d11.DRIVER_TYPE_HARDWARE, - 0, - ) - if err != nil { - return nil, err - } - // Don't need it. - d3d11.IUnknownRelease(unsafe.Pointer(ctx), ctx.Vtbl.Release) - return &d3d11Context{dev: dev}, nil - } -} - -func (c *d3d11Context) API() gpu.API { - return gpu.Direct3D11{Device: unsafe.Pointer(c.dev)} -} - -func (c *d3d11Context) MakeCurrent() error { - return nil -} - -func (c *d3d11Context) ReleaseCurrent() { -} - -func (c *d3d11Context) Release() { - d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release) - c.dev = nil -} diff --git a/gio/gpu/internal/d3d11/d3d11.go b/gio/gpu/internal/d3d11/d3d11.go deleted file mode 100644 index 3ddf7c3..0000000 --- a/gio/gpu/internal/d3d11/d3d11.go +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// This file exists so this package builds on non-Windows platforms. - -package d3d11 diff --git a/gio/gpu/internal/d3d11/d3d11_windows.go b/gio/gpu/internal/d3d11/d3d11_windows.go deleted file mode 100644 index f2523ea..0000000 --- a/gio/gpu/internal/d3d11/d3d11_windows.go +++ /dev/null @@ -1,871 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package d3d11 - -import ( - "errors" - "fmt" - "image" - "math" - "math/bits" - "unsafe" - - "golang.org/x/sys/windows" - - "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" - "github.com/p9c/p9/pkg/gel/gio/internal/d3d11" - "github.com/p9c/p9/pkg/gel/gio/shader" -) - -type Backend struct { - dev *d3d11.Device - ctx *d3d11.DeviceContext - - // Temporary storage to avoid garbage. - clearColor [4]float32 - viewport d3d11.VIEWPORT - - pipeline *Pipeline - vert struct { - buffer *Buffer - offset int - } - - program *Program - - caps driver.Caps - - floatFormat uint32 -} - -type Pipeline struct { - vert *d3d11.VertexShader - frag *d3d11.PixelShader - layout *d3d11.InputLayout - blend *d3d11.BlendState - stride int - topology driver.Topology -} - -type Texture struct { - backend *Backend - format uint32 - bindings driver.BufferBinding - tex *d3d11.Texture2D - sampler *d3d11.SamplerState - resView *d3d11.ShaderResourceView - uaView *d3d11.UnorderedAccessView - renderTarget *d3d11.RenderTargetView - - width int - height int - mipmap bool - foreign bool -} - -type VertexShader struct { - backend *Backend - shader *d3d11.VertexShader - src shader.Sources -} - -type FragmentShader struct { - backend *Backend - shader *d3d11.PixelShader -} - -type Program struct { - backend *Backend - shader *d3d11.ComputeShader -} - -type Buffer struct { - backend *Backend - bind uint32 - buf *d3d11.Buffer - resView *d3d11.ShaderResourceView - uaView *d3d11.UnorderedAccessView - size int - immutable bool -} - -func init() { - driver.NewDirect3D11Device = newDirect3D11Device -} - -func detectFloatFormat(dev *d3d11.Device) (uint32, bool) { - formats := []uint32{ - d3d11.DXGI_FORMAT_R16_FLOAT, - d3d11.DXGI_FORMAT_R32_FLOAT, - d3d11.DXGI_FORMAT_R16G16_FLOAT, - d3d11.DXGI_FORMAT_R32G32_FLOAT, - // These last two are really wasteful, but c'est la vie. - d3d11.DXGI_FORMAT_R16G16B16A16_FLOAT, - d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT, - } - for _, format := range formats { - need := uint32(d3d11.FORMAT_SUPPORT_TEXTURE2D | d3d11.FORMAT_SUPPORT_RENDER_TARGET) - if support, _ := dev.CheckFormatSupport(format); support&need == need { - return format, true - } - } - return 0, false -} - -func newDirect3D11Device(api driver.Direct3D11) (driver.Device, error) { - dev := (*d3d11.Device)(api.Device) - b := &Backend{ - dev: dev, - ctx: dev.GetImmediateContext(), - caps: driver.Caps{ - MaxTextureSize: 2048, // 9.1 maximum - Features: driver.FeatureSRGB, - }, - } - featLvl := dev.GetFeatureLevel() - switch { - case featLvl < d3d11.FEATURE_LEVEL_9_1: - d3d11.IUnknownRelease(unsafe.Pointer(dev), dev.Vtbl.Release) - d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release) - return nil, fmt.Errorf("d3d11: feature level too low: %d", featLvl) - case featLvl >= d3d11.FEATURE_LEVEL_11_0: - b.caps.MaxTextureSize = 16384 - b.caps.Features |= driver.FeatureCompute - case featLvl >= d3d11.FEATURE_LEVEL_9_3: - b.caps.MaxTextureSize = 4096 - } - if fmt, ok := detectFloatFormat(dev); ok { - b.floatFormat = fmt - b.caps.Features |= driver.FeatureFloatRenderTargets - } - // Disable backface culling to match OpenGL. - state, err := dev.CreateRasterizerState(&d3d11.RASTERIZER_DESC{ - CullMode: d3d11.CULL_NONE, - FillMode: d3d11.FILL_SOLID, - }) - if err != nil { - return nil, err - } - defer d3d11.IUnknownRelease(unsafe.Pointer(state), state.Vtbl.Release) - b.ctx.RSSetState(state) - return b, nil -} - -func (b *Backend) BeginFrame(target driver.RenderTarget, clear bool, viewport image.Point) driver.Texture { - var renderTarget *d3d11.RenderTargetView - if target != nil { - switch t := target.(type) { - case driver.Direct3D11RenderTarget: - renderTarget = (*d3d11.RenderTargetView)(t.RenderTarget) - case *Texture: - renderTarget = t.renderTarget - default: - panic(fmt.Errorf("d3d11: invalid render target type: %T", target)) - } - } - b.ctx.OMSetRenderTargets(renderTarget, nil) - return &Texture{backend: b, renderTarget: renderTarget, foreign: true} -} - -func (b *Backend) CopyTexture(dstTex driver.Texture, dstOrigin image.Point, srcTex driver.Texture, srcRect image.Rectangle) { - dst := (*d3d11.Resource)(unsafe.Pointer(dstTex.(*Texture).tex)) - src := (*d3d11.Resource)(srcTex.(*Texture).tex) - b.ctx.CopySubresourceRegion( - dst, - 0, // Destination subresource. - uint32(dstOrigin.X), uint32(dstOrigin.Y), 0, // Destination coordinates (x, y, z). - src, - 0, // Source subresource. - &d3d11.BOX{ - Left: uint32(srcRect.Min.X), - Top: uint32(srcRect.Min.Y), - Right: uint32(srcRect.Max.X), - Bottom: uint32(srcRect.Max.Y), - Front: 0, - Back: 1, - }, - ) -} - -func (b *Backend) EndFrame() { -} - -func (b *Backend) Caps() driver.Caps { - return b.caps -} - -func (b *Backend) NewTimer() driver.Timer { - panic("timers not supported") -} - -func (b *Backend) IsTimeContinuous() bool { - panic("timers not supported") -} - -func (b *Backend) Release() { - d3d11.IUnknownRelease(unsafe.Pointer(b.ctx), b.ctx.Vtbl.Release) - *b = Backend{} -} - -func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, minFilter, magFilter driver.TextureFilter, bindings driver.BufferBinding) (driver.Texture, error) { - var d3dfmt uint32 - switch format { - case driver.TextureFormatFloat: - d3dfmt = b.floatFormat - case driver.TextureFormatSRGBA: - d3dfmt = d3d11.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB - case driver.TextureFormatRGBA8: - d3dfmt = d3d11.DXGI_FORMAT_R8G8B8A8_UNORM - default: - return nil, fmt.Errorf("unsupported texture format %d", format) - } - bindFlags := convBufferBinding(bindings) - miscFlags := uint32(0) - mipmap := minFilter == driver.FilterLinearMipmapLinear - nmipmaps := 1 - if mipmap { - // Flags required by ID3D11DeviceContext::GenerateMips. - bindFlags |= d3d11.BIND_SHADER_RESOURCE | d3d11.BIND_RENDER_TARGET - miscFlags |= d3d11.RESOURCE_MISC_GENERATE_MIPS - dim := max(height, width) - log2 := 32 - bits.LeadingZeros32(uint32(dim)) - 1 - nmipmaps = log2 + 1 - } - tex, err := b.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{ - Width: uint32(width), - Height: uint32(height), - MipLevels: uint32(nmipmaps), - ArraySize: 1, - Format: d3dfmt, - SampleDesc: d3d11.DXGI_SAMPLE_DESC{ - Count: 1, - Quality: 0, - }, - BindFlags: bindFlags, - MiscFlags: miscFlags, - }) - if err != nil { - return nil, err - } - var ( - sampler *d3d11.SamplerState - resView *d3d11.ShaderResourceView - uaView *d3d11.UnorderedAccessView - fbo *d3d11.RenderTargetView - ) - if bindings&driver.BufferBindingTexture != 0 { - var filter uint32 - switch { - case minFilter == driver.FilterNearest && magFilter == driver.FilterNearest: - filter = d3d11.FILTER_MIN_MAG_MIP_POINT - case minFilter == driver.FilterLinear && magFilter == driver.FilterLinear: - filter = d3d11.FILTER_MIN_MAG_LINEAR_MIP_POINT - case minFilter == driver.FilterLinearMipmapLinear && magFilter == driver.FilterLinear: - filter = d3d11.FILTER_MIN_MAG_MIP_LINEAR - default: - d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) - return nil, fmt.Errorf("unsupported texture filter combination %d, %d", minFilter, magFilter) - } - var err error - sampler, err = b.dev.CreateSamplerState(&d3d11.SAMPLER_DESC{ - Filter: filter, - AddressU: d3d11.TEXTURE_ADDRESS_CLAMP, - AddressV: d3d11.TEXTURE_ADDRESS_CLAMP, - AddressW: d3d11.TEXTURE_ADDRESS_CLAMP, - MaxAnisotropy: 1, - MinLOD: -math.MaxFloat32, - MaxLOD: math.MaxFloat32, - }) - if err != nil { - d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) - return nil, err - } - resView, err = b.dev.CreateShaderResourceView( - (*d3d11.Resource)(unsafe.Pointer(tex)), - unsafe.Pointer(&d3d11.SHADER_RESOURCE_VIEW_DESC_TEX2D{ - SHADER_RESOURCE_VIEW_DESC: d3d11.SHADER_RESOURCE_VIEW_DESC{ - Format: d3dfmt, - ViewDimension: d3d11.SRV_DIMENSION_TEXTURE2D, - }, - Texture2D: d3d11.TEX2D_SRV{ - MostDetailedMip: 0, - MipLevels: ^uint32(0), - }, - }), - ) - if err != nil { - d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) - d3d11.IUnknownRelease(unsafe.Pointer(sampler), sampler.Vtbl.Release) - return nil, err - } - } - if bindings&driver.BufferBindingShaderStorageWrite != 0 { - uaView, err = b.dev.CreateUnorderedAccessView( - (*d3d11.Resource)(unsafe.Pointer(tex)), - unsafe.Pointer(&d3d11.UNORDERED_ACCESS_VIEW_DESC_TEX2D{ - UNORDERED_ACCESS_VIEW_DESC: d3d11.UNORDERED_ACCESS_VIEW_DESC{ - Format: d3dfmt, - ViewDimension: d3d11.UAV_DIMENSION_TEXTURE2D, - }, - Texture2D: d3d11.TEX2D_UAV{ - MipSlice: 0, - }, - }), - ) - if err != nil { - if sampler != nil { - d3d11.IUnknownRelease(unsafe.Pointer(sampler), sampler.Vtbl.Release) - } - if resView != nil { - d3d11.IUnknownRelease(unsafe.Pointer(resView), resView.Vtbl.Release) - } - d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) - return nil, err - } - } - if bindings&driver.BufferBindingFramebuffer != 0 { - resource := (*d3d11.Resource)(unsafe.Pointer(tex)) - fbo, err = b.dev.CreateRenderTargetView(resource) - if err != nil { - if uaView != nil { - d3d11.IUnknownRelease(unsafe.Pointer(uaView), uaView.Vtbl.Release) - } - if sampler != nil { - d3d11.IUnknownRelease(unsafe.Pointer(sampler), sampler.Vtbl.Release) - } - if resView != nil { - d3d11.IUnknownRelease(unsafe.Pointer(resView), resView.Vtbl.Release) - } - d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) - return nil, err - } - } - return &Texture{backend: b, format: d3dfmt, tex: tex, sampler: sampler, resView: resView, uaView: uaView, renderTarget: fbo, bindings: bindings, width: width, height: height, mipmap: mipmap}, nil -} - -func (b *Backend) newInputLayout(vertexShader shader.Sources, layout []driver.InputDesc) (*d3d11.InputLayout, error) { - if len(vertexShader.Inputs) != len(layout) { - return nil, fmt.Errorf("NewInputLayout: got %d inputs, expected %d", len(layout), len(vertexShader.Inputs)) - } - descs := make([]d3d11.INPUT_ELEMENT_DESC, len(layout)) - for i, l := range layout { - inp := vertexShader.Inputs[i] - cname, err := windows.BytePtrFromString(inp.Semantic) - if err != nil { - return nil, err - } - var format uint32 - switch l.Type { - case shader.DataTypeFloat: - switch l.Size { - case 1: - format = d3d11.DXGI_FORMAT_R32_FLOAT - case 2: - format = d3d11.DXGI_FORMAT_R32G32_FLOAT - case 3: - format = d3d11.DXGI_FORMAT_R32G32B32_FLOAT - case 4: - format = d3d11.DXGI_FORMAT_R32G32B32A32_FLOAT - default: - panic("unsupported data size") - } - case shader.DataTypeShort: - switch l.Size { - case 1: - format = d3d11.DXGI_FORMAT_R16_SINT - case 2: - format = d3d11.DXGI_FORMAT_R16G16_SINT - default: - panic("unsupported data size") - } - default: - panic("unsupported data type") - } - descs[i] = d3d11.INPUT_ELEMENT_DESC{ - SemanticName: cname, - SemanticIndex: uint32(inp.SemanticIndex), - Format: format, - AlignedByteOffset: uint32(l.Offset), - } - } - return b.dev.CreateInputLayout(descs, []byte(vertexShader.DXBC)) -} - -func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, error) { - return b.newBuffer(typ, size, nil, false) -} - -func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, data []byte) (driver.Buffer, error) { - return b.newBuffer(typ, len(data), data, true) -} - -func (b *Backend) newBuffer(typ driver.BufferBinding, size int, data []byte, immutable bool) (*Buffer, error) { - if typ&driver.BufferBindingUniforms != 0 { - if typ != driver.BufferBindingUniforms { - return nil, errors.New("uniform buffers cannot have other bindings") - } - if size%16 != 0 { - return nil, fmt.Errorf("constant buffer size is %d, expected a multiple of 16", size) - } - } - bind := convBufferBinding(typ) - var usage, miscFlags, cpuFlags uint32 - if immutable { - usage = d3d11.USAGE_IMMUTABLE - } - if typ&driver.BufferBindingShaderStorageWrite != 0 { - cpuFlags = d3d11.CPU_ACCESS_READ - } - if typ&(driver.BufferBindingShaderStorageRead|driver.BufferBindingShaderStorageWrite) != 0 { - miscFlags |= d3d11.RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS - } - buf, err := b.dev.CreateBuffer(&d3d11.BUFFER_DESC{ - ByteWidth: uint32(size), - Usage: usage, - BindFlags: bind, - CPUAccessFlags: cpuFlags, - MiscFlags: miscFlags, - }, data) - if err != nil { - return nil, err - } - var ( - resView *d3d11.ShaderResourceView - uaView *d3d11.UnorderedAccessView - ) - if typ&driver.BufferBindingShaderStorageWrite != 0 { - uaView, err = b.dev.CreateUnorderedAccessView( - (*d3d11.Resource)(unsafe.Pointer(buf)), - unsafe.Pointer(&d3d11.UNORDERED_ACCESS_VIEW_DESC_BUFFER{ - UNORDERED_ACCESS_VIEW_DESC: d3d11.UNORDERED_ACCESS_VIEW_DESC{ - Format: d3d11.DXGI_FORMAT_R32_TYPELESS, - ViewDimension: d3d11.UAV_DIMENSION_BUFFER, - }, - Buffer: d3d11.BUFFER_UAV{ - FirstElement: 0, - NumElements: uint32(size / 4), - Flags: d3d11.BUFFER_UAV_FLAG_RAW, - }, - }), - ) - if err != nil { - d3d11.IUnknownRelease(unsafe.Pointer(buf), buf.Vtbl.Release) - return nil, err - } - } else if typ&driver.BufferBindingShaderStorageRead != 0 { - resView, err = b.dev.CreateShaderResourceView( - (*d3d11.Resource)(unsafe.Pointer(buf)), - unsafe.Pointer(&d3d11.SHADER_RESOURCE_VIEW_DESC_BUFFEREX{ - SHADER_RESOURCE_VIEW_DESC: d3d11.SHADER_RESOURCE_VIEW_DESC{ - Format: d3d11.DXGI_FORMAT_R32_TYPELESS, - ViewDimension: d3d11.SRV_DIMENSION_BUFFEREX, - }, - Buffer: d3d11.BUFFEREX_SRV{ - FirstElement: 0, - NumElements: uint32(size / 4), - Flags: d3d11.BUFFEREX_SRV_FLAG_RAW, - }, - }), - ) - if err != nil { - d3d11.IUnknownRelease(unsafe.Pointer(buf), buf.Vtbl.Release) - return nil, err - } - } - return &Buffer{backend: b, buf: buf, bind: bind, size: size, resView: resView, uaView: uaView, immutable: immutable}, nil -} - -func (b *Backend) NewComputeProgram(shader shader.Sources) (driver.Program, error) { - cs, err := b.dev.CreateComputeShader([]byte(shader.DXBC)) - if err != nil { - return nil, err - } - return &Program{backend: b, shader: cs}, nil -} - -func (b *Backend) NewPipeline(desc driver.PipelineDesc) (driver.Pipeline, error) { - vsh := desc.VertexShader.(*VertexShader) - fsh := desc.FragmentShader.(*FragmentShader) - blend, err := b.newBlendState(desc.BlendDesc) - if err != nil { - return nil, err - } - var layout *d3d11.InputLayout - if l := desc.VertexLayout; l.Stride > 0 { - var err error - layout, err = b.newInputLayout(vsh.src, l.Inputs) - if err != nil { - d3d11.IUnknownRelease(unsafe.Pointer(blend), blend.Vtbl.AddRef) - return nil, err - } - } - - // Retain shaders. - vshRef := vsh.shader - fshRef := fsh.shader - d3d11.IUnknownAddRef(unsafe.Pointer(vshRef), vshRef.Vtbl.AddRef) - d3d11.IUnknownAddRef(unsafe.Pointer(fshRef), fshRef.Vtbl.AddRef) - - return &Pipeline{ - vert: vshRef, - frag: fshRef, - layout: layout, - stride: desc.VertexLayout.Stride, - blend: blend, - topology: desc.Topology, - }, nil -} - -func (b *Backend) newBlendState(desc driver.BlendDesc) (*d3d11.BlendState, error) { - var d3ddesc d3d11.BLEND_DESC - t0 := &d3ddesc.RenderTarget[0] - t0.RenderTargetWriteMask = d3d11.COLOR_WRITE_ENABLE_ALL - t0.BlendOp = d3d11.BLEND_OP_ADD - t0.BlendOpAlpha = d3d11.BLEND_OP_ADD - if desc.Enable { - t0.BlendEnable = 1 - } - scol, salpha := toBlendFactor(desc.SrcFactor) - dcol, dalpha := toBlendFactor(desc.DstFactor) - t0.SrcBlend = scol - t0.SrcBlendAlpha = salpha - t0.DestBlend = dcol - t0.DestBlendAlpha = dalpha - return b.dev.CreateBlendState(&d3ddesc) -} - -func (b *Backend) NewVertexShader(src shader.Sources) (driver.VertexShader, error) { - vs, err := b.dev.CreateVertexShader([]byte(src.DXBC)) - if err != nil { - return nil, err - } - return &VertexShader{b, vs, src}, nil -} - -func (b *Backend) NewFragmentShader(src shader.Sources) (driver.FragmentShader, error) { - fs, err := b.dev.CreatePixelShader([]byte(src.DXBC)) - if err != nil { - return nil, err - } - return &FragmentShader{b, fs}, nil -} - -func (b *Backend) Viewport(x, y, width, height int) { - b.viewport = d3d11.VIEWPORT{ - TopLeftX: float32(x), - TopLeftY: float32(y), - Width: float32(width), - Height: float32(height), - MinDepth: 0.0, - MaxDepth: 1.0, - } - b.ctx.RSSetViewports(&b.viewport) -} - -func (b *Backend) DrawArrays(off, count int) { - b.prepareDraw() - b.ctx.Draw(uint32(count), uint32(off)) -} - -func (b *Backend) DrawElements(off, count int) { - b.prepareDraw() - b.ctx.DrawIndexed(uint32(count), uint32(off), 0) -} - -func (b *Backend) prepareDraw() { - p := b.pipeline - if p == nil { - return - } - b.ctx.VSSetShader(p.vert) - b.ctx.PSSetShader(p.frag) - b.ctx.IASetInputLayout(p.layout) - b.ctx.OMSetBlendState(p.blend, nil, 0xffffffff) - if b.vert.buffer != nil { - b.ctx.IASetVertexBuffers(b.vert.buffer.buf, uint32(p.stride), uint32(b.vert.offset)) - } - var topology uint32 - switch p.topology { - case driver.TopologyTriangles: - topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLELIST - case driver.TopologyTriangleStrip: - topology = d3d11.PRIMITIVE_TOPOLOGY_TRIANGLESTRIP - default: - panic("unsupported draw mode") - } - b.ctx.IASetPrimitiveTopology(topology) -} - -func (b *Backend) BindImageTexture(unit int, tex driver.Texture) { - t := tex.(*Texture) - if t.uaView != nil { - b.ctx.CSSetUnorderedAccessViews(uint32(unit), t.uaView) - } else { - b.ctx.CSSetShaderResources(uint32(unit), t.resView) - } -} - -func (b *Backend) DispatchCompute(x, y, z int) { - b.ctx.CSSetShader(b.program.shader) - b.ctx.Dispatch(uint32(x), uint32(y), uint32(z)) -} - -func (t *Texture) Upload(offset, size image.Point, pixels []byte, stride int) { - if stride == 0 { - stride = size.X * 4 - } - dst := &d3d11.BOX{ - Left: uint32(offset.X), - Top: uint32(offset.Y), - Right: uint32(offset.X + size.X), - Bottom: uint32(offset.Y + size.Y), - Front: 0, - Back: 1, - } - res := (*d3d11.Resource)(unsafe.Pointer(t.tex)) - t.backend.ctx.UpdateSubresource(res, dst, uint32(stride), uint32(len(pixels)), pixels) - if t.mipmap { - t.backend.ctx.GenerateMips(t.resView) - } -} - -func (t *Texture) Release() { - if t.foreign { - panic("texture not created by NewTexture") - } - if t.renderTarget != nil { - d3d11.IUnknownRelease(unsafe.Pointer(t.renderTarget), t.renderTarget.Vtbl.Release) - } - if t.sampler != nil { - d3d11.IUnknownRelease(unsafe.Pointer(t.sampler), t.sampler.Vtbl.Release) - } - if t.resView != nil { - d3d11.IUnknownRelease(unsafe.Pointer(t.resView), t.resView.Vtbl.Release) - } - if t.uaView != nil { - d3d11.IUnknownRelease(unsafe.Pointer(t.uaView), t.uaView.Vtbl.Release) - } - d3d11.IUnknownRelease(unsafe.Pointer(t.tex), t.tex.Vtbl.Release) - *t = Texture{} -} - -func (b *Backend) PrepareTexture(tex driver.Texture) {} - -func (b *Backend) BindTexture(unit int, tex driver.Texture) { - t := tex.(*Texture) - b.ctx.PSSetSamplers(uint32(unit), t.sampler) - b.ctx.PSSetShaderResources(uint32(unit), t.resView) -} - -func (b *Backend) BindPipeline(pipe driver.Pipeline) { - b.pipeline = pipe.(*Pipeline) -} - -func (b *Backend) BindProgram(prog driver.Program) { - b.program = prog.(*Program) -} - -func (s *VertexShader) Release() { - d3d11.IUnknownRelease(unsafe.Pointer(s.shader), s.shader.Vtbl.Release) - *s = VertexShader{} -} - -func (s *FragmentShader) Release() { - d3d11.IUnknownRelease(unsafe.Pointer(s.shader), s.shader.Vtbl.Release) - *s = FragmentShader{} -} - -func (s *Program) Release() { - d3d11.IUnknownRelease(unsafe.Pointer(s.shader), s.shader.Vtbl.Release) - *s = Program{} -} - -func (p *Pipeline) Release() { - d3d11.IUnknownRelease(unsafe.Pointer(p.vert), p.vert.Vtbl.Release) - d3d11.IUnknownRelease(unsafe.Pointer(p.frag), p.frag.Vtbl.Release) - d3d11.IUnknownRelease(unsafe.Pointer(p.blend), p.blend.Vtbl.Release) - if l := p.layout; l != nil { - d3d11.IUnknownRelease(unsafe.Pointer(l), l.Vtbl.Release) - } - *p = Pipeline{} -} - -func (b *Backend) BindStorageBuffer(binding int, buffer driver.Buffer) { - buf := buffer.(*Buffer) - if buf.resView != nil { - b.ctx.CSSetShaderResources(uint32(binding), buf.resView) - } else { - b.ctx.CSSetUnorderedAccessViews(uint32(binding), buf.uaView) - } -} - -func (b *Backend) BindUniforms(buffer driver.Buffer) { - buf := buffer.(*Buffer) - b.ctx.VSSetConstantBuffers(buf.buf) - b.ctx.PSSetConstantBuffers(buf.buf) -} - -func (b *Backend) BindVertexBuffer(buf driver.Buffer, offset int) { - b.vert.buffer = buf.(*Buffer) - b.vert.offset = offset -} - -func (b *Backend) BindIndexBuffer(buf driver.Buffer) { - b.ctx.IASetIndexBuffer(buf.(*Buffer).buf, d3d11.DXGI_FORMAT_R16_UINT, 0) -} - -func (b *Buffer) Download(dst []byte) error { - res := (*d3d11.Resource)(unsafe.Pointer(b.buf)) - resMap, err := b.backend.ctx.Map(res, 0, d3d11.MAP_READ, 0) - if err != nil { - return fmt.Errorf("d3d11: %v", err) - } - defer b.backend.ctx.Unmap(res, 0) - data := sliceOf(resMap.PData, len(dst)) - copy(dst, data) - return nil -} - -func (b *Buffer) Upload(data []byte) { - var dst *d3d11.BOX - if len(data) < b.size { - dst = &d3d11.BOX{ - Left: 0, - Right: uint32(len(data)), - Top: 0, - Bottom: 1, - Front: 0, - Back: 1, - } - } - b.backend.ctx.UpdateSubresource((*d3d11.Resource)(unsafe.Pointer(b.buf)), dst, 0, 0, data) -} - -func (b *Buffer) Release() { - if b.resView != nil { - d3d11.IUnknownRelease(unsafe.Pointer(b.resView), b.resView.Vtbl.Release) - } - if b.uaView != nil { - d3d11.IUnknownRelease(unsafe.Pointer(b.uaView), b.uaView.Vtbl.Release) - } - d3d11.IUnknownRelease(unsafe.Pointer(b.buf), b.buf.Vtbl.Release) - *b = Buffer{} -} - -func (t *Texture) ReadPixels(src image.Rectangle, pixels []byte, stride int) error { - w, h := src.Dx(), src.Dy() - tex, err := t.backend.dev.CreateTexture2D(&d3d11.TEXTURE2D_DESC{ - Width: uint32(w), - Height: uint32(h), - MipLevels: 1, - ArraySize: 1, - Format: t.format, - SampleDesc: d3d11.DXGI_SAMPLE_DESC{ - Count: 1, - Quality: 0, - }, - Usage: d3d11.USAGE_STAGING, - CPUAccessFlags: d3d11.CPU_ACCESS_READ, - }) - if err != nil { - return fmt.Errorf("ReadPixels: %v", err) - } - defer d3d11.IUnknownRelease(unsafe.Pointer(tex), tex.Vtbl.Release) - res := (*d3d11.Resource)(unsafe.Pointer(tex)) - t.backend.ctx.CopySubresourceRegion( - res, - 0, // Destination subresource. - 0, 0, 0, // Destination coordinates (x, y, z). - (*d3d11.Resource)(t.tex), - 0, // Source subresource. - &d3d11.BOX{ - Left: uint32(src.Min.X), - Top: uint32(src.Min.Y), - Right: uint32(src.Max.X), - Bottom: uint32(src.Max.Y), - Front: 0, - Back: 1, - }, - ) - resMap, err := t.backend.ctx.Map(res, 0, d3d11.MAP_READ, 0) - if err != nil { - return fmt.Errorf("ReadPixels: %v", err) - } - defer t.backend.ctx.Unmap(res, 0) - srcPitch := stride - dstPitch := int(resMap.RowPitch) - mapSize := dstPitch * h - data := sliceOf(resMap.PData, mapSize) - width := w * 4 - for r := range h { - pixels := pixels[r*srcPitch:] - copy(pixels[:width], data[r*dstPitch:]) - } - return nil -} - -func (b *Backend) BeginCompute() { -} - -func (b *Backend) EndCompute() { -} - -func (b *Backend) BeginRenderPass(tex driver.Texture, d driver.LoadDesc) { - t := tex.(*Texture) - b.ctx.OMSetRenderTargets(t.renderTarget, nil) - if d.Action == driver.LoadActionClear { - c := d.ClearColor - b.clearColor = [4]float32{c.R, c.G, c.B, c.A} - b.ctx.ClearRenderTargetView(t.renderTarget, &b.clearColor) - } -} - -func (b *Backend) EndRenderPass() { -} - -func (f *Texture) ImplementsRenderTarget() {} - -func convBufferBinding(typ driver.BufferBinding) uint32 { - var bindings uint32 - if typ&driver.BufferBindingVertices != 0 { - bindings |= d3d11.BIND_VERTEX_BUFFER - } - if typ&driver.BufferBindingIndices != 0 { - bindings |= d3d11.BIND_INDEX_BUFFER - } - if typ&driver.BufferBindingUniforms != 0 { - bindings |= d3d11.BIND_CONSTANT_BUFFER - } - if typ&driver.BufferBindingTexture != 0 { - bindings |= d3d11.BIND_SHADER_RESOURCE - } - if typ&driver.BufferBindingFramebuffer != 0 { - bindings |= d3d11.BIND_RENDER_TARGET - } - if typ&driver.BufferBindingShaderStorageWrite != 0 { - bindings |= d3d11.BIND_UNORDERED_ACCESS - } else if typ&driver.BufferBindingShaderStorageRead != 0 { - bindings |= d3d11.BIND_SHADER_RESOURCE - } - return bindings -} - -func toBlendFactor(f driver.BlendFactor) (uint32, uint32) { - switch f { - case driver.BlendFactorOne: - return d3d11.BLEND_ONE, d3d11.BLEND_ONE - case driver.BlendFactorOneMinusSrcAlpha: - return d3d11.BLEND_INV_SRC_ALPHA, d3d11.BLEND_INV_SRC_ALPHA - case driver.BlendFactorZero: - return d3d11.BLEND_ZERO, d3d11.BLEND_ZERO - case driver.BlendFactorDstColor: - return d3d11.BLEND_DEST_COLOR, d3d11.BLEND_DEST_ALPHA - default: - panic("unsupported blend source factor") - } -} - -// sliceOf returns a slice from a (native) pointer. -func sliceOf(ptr uintptr, cap int) []byte { - return unsafe.Slice((*byte)(unsafe.Pointer(ptr)), cap) -} diff --git a/gio/gpu/internal/driver/api.go b/gio/gpu/internal/driver/api.go deleted file mode 100644 index 20148a0..0000000 --- a/gio/gpu/internal/driver/api.go +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package driver - -import ( - "fmt" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/internal/gl" -) - -// See gpu/api.go for documentation for the API types. - -type API interface { - implementsAPI() -} - -type RenderTarget interface { - ImplementsRenderTarget() -} - -type OpenGLRenderTarget gl.Framebuffer - -type Direct3D11RenderTarget struct { - // RenderTarget is a *ID3D11RenderTargetView. - RenderTarget unsafe.Pointer -} - -type MetalRenderTarget struct { - // Texture is a MTLTexture. - Texture uintptr -} - -type VulkanRenderTarget struct { - // WaitSem is a VkSemaphore that must signaled before accessing Framebuffer. - WaitSem uint64 - // SignalSem is a VkSemaphore that signal access to Framebuffer is complete. - SignalSem uint64 - // Fence is a VkFence that is set when all commands to Framebuffer has completed. - Fence uint64 - // Image is the VkImage to render into. - Image uint64 - // Framebuffer is a VkFramebuffer for Image. - Framebuffer uint64 -} - -type OpenGL struct { - // ES forces the use of ANGLE OpenGL ES libraries on macOS. It is - // ignored on all other platforms. - ES bool - // Context contains the WebGL context for WebAssembly platforms. It is - // empty for all other platforms; an OpenGL context is assumed current when - // calling NewDevice. - Context gl.Context - // Shared instructs users of the context to restore the GL state after - // use. - Shared bool -} - -type Direct3D11 struct { - // Device contains a *ID3D11Device. - Device unsafe.Pointer -} - -type Metal struct { - // Device is an MTLDevice. - Device uintptr - // Queue is a MTLCommandQueue. - Queue uintptr - // PixelFormat is the MTLPixelFormat of the default framebuffer. - PixelFormat int -} - -type Vulkan struct { - // PhysDevice is a VkPhysicalDevice. - PhysDevice unsafe.Pointer - // Device is a VkDevice. - Device unsafe.Pointer - // QueueFamily is the queue familily index of the queue. - QueueFamily int - // QueueIndex is the logical queue index of the queue. - QueueIndex int - // Format is a VkFormat that matches render targets. - Format int -} - -// API specific device constructors. -var ( - NewOpenGLDevice func(api OpenGL) (Device, error) - NewDirect3D11Device func(api Direct3D11) (Device, error) - NewMetalDevice func(api Metal) (Device, error) - NewVulkanDevice func(api Vulkan) (Device, error) -) - -// NewDevice creates a new Device given the api. -// -// Note that the device does not assume ownership of the resources contained in -// api; the caller must ensure the resources are valid until the device is -// released. -func NewDevice(api API) (Device, error) { - switch api := api.(type) { - case OpenGL: - if NewOpenGLDevice != nil { - return NewOpenGLDevice(api) - } - case Direct3D11: - if NewDirect3D11Device != nil { - return NewDirect3D11Device(api) - } - case Metal: - if NewMetalDevice != nil { - return NewMetalDevice(api) - } - case Vulkan: - if NewVulkanDevice != nil { - return NewVulkanDevice(api) - } - } - return nil, fmt.Errorf("driver: no driver available for the API %T", api) -} - -func (OpenGL) implementsAPI() {} -func (Direct3D11) implementsAPI() {} -func (Metal) implementsAPI() {} -func (Vulkan) implementsAPI() {} -func (OpenGLRenderTarget) ImplementsRenderTarget() {} -func (Direct3D11RenderTarget) ImplementsRenderTarget() {} -func (MetalRenderTarget) ImplementsRenderTarget() {} -func (VulkanRenderTarget) ImplementsRenderTarget() {} diff --git a/gio/gpu/internal/driver/driver.go b/gio/gpu/internal/driver/driver.go deleted file mode 100644 index bfaedd2..0000000 --- a/gio/gpu/internal/driver/driver.go +++ /dev/null @@ -1,240 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package driver - -import ( - "errors" - "image" - "time" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/shader" -) - -// Device represents the abstraction of underlying GPU -// APIs such as OpenGL, Direct3D useful for rendering Gio -// operations. -type Device interface { - BeginFrame(target RenderTarget, clear bool, viewport image.Point) Texture - EndFrame() - Caps() Caps - NewTimer() Timer - // IsContinuousTime reports whether all timer measurements - // are valid at the point of call. - IsTimeContinuous() bool - NewTexture(format TextureFormat, width, height int, minFilter, magFilter TextureFilter, bindings BufferBinding) (Texture, error) - NewImmutableBuffer(typ BufferBinding, data []byte) (Buffer, error) - NewBuffer(typ BufferBinding, size int) (Buffer, error) - NewComputeProgram(shader shader.Sources) (Program, error) - NewVertexShader(src shader.Sources) (VertexShader, error) - NewFragmentShader(src shader.Sources) (FragmentShader, error) - NewPipeline(desc PipelineDesc) (Pipeline, error) - - Viewport(x, y, width, height int) - DrawArrays(off, count int) - DrawElements(off, count int) - - BeginRenderPass(t Texture, desc LoadDesc) - EndRenderPass() - PrepareTexture(t Texture) - BindProgram(p Program) - BindPipeline(p Pipeline) - BindTexture(unit int, t Texture) - BindVertexBuffer(b Buffer, offset int) - BindIndexBuffer(b Buffer) - BindImageTexture(unit int, texture Texture) - BindUniforms(buf Buffer) - BindStorageBuffer(binding int, buf Buffer) - - BeginCompute() - EndCompute() - CopyTexture(dst Texture, dstOrigin image.Point, src Texture, srcRect image.Rectangle) - DispatchCompute(x, y, z int) - - Release() -} - -var ErrDeviceLost = errors.New("GPU device lost") - -type LoadDesc struct { - Action LoadAction - ClearColor f32color.RGBA -} - -type Pipeline interface { - Release() -} - -type PipelineDesc struct { - VertexShader VertexShader - FragmentShader FragmentShader - VertexLayout VertexLayout - BlendDesc BlendDesc - PixelFormat TextureFormat - Topology Topology -} - -type VertexLayout struct { - Inputs []InputDesc - Stride int -} - -// InputDesc describes a vertex attribute as laid out in a Buffer. -type InputDesc struct { - Type shader.DataType - Size int - - Offset int -} - -type BlendDesc struct { - Enable bool - SrcFactor, DstFactor BlendFactor -} - -type BlendFactor uint8 - -type Topology uint8 - -type ( - TextureFilter uint8 - TextureFormat uint8 -) - -type BufferBinding uint8 - -type LoadAction uint8 - -type Features uint - -type Caps struct { - // BottomLeftOrigin is true if the driver has the origin in the lower left - // corner. The OpenGL driver returns true. - BottomLeftOrigin bool - Features Features - MaxTextureSize int -} - -type VertexShader interface { - Release() -} - -type FragmentShader interface { - Release() -} - -type Program interface { - Release() -} - -type Buffer interface { - Release() - Upload(data []byte) - Download(data []byte) error -} - -type Timer interface { - Begin() - End() - Duration() (time.Duration, bool) - Release() -} - -type Texture interface { - RenderTarget - Upload(offset, size image.Point, pixels []byte, stride int) - ReadPixels(src image.Rectangle, pixels []byte, stride int) error - Release() -} - -const ( - BufferBindingIndices BufferBinding = 1 << iota - BufferBindingVertices - BufferBindingUniforms - BufferBindingTexture - BufferBindingFramebuffer - BufferBindingShaderStorageRead - BufferBindingShaderStorageWrite -) - -const ( - TextureFormatSRGBA TextureFormat = iota - TextureFormatFloat - TextureFormatRGBA8 - // TextureFormatOutput denotes the format used by the output framebuffer. - TextureFormatOutput -) - -const ( - FilterNearest TextureFilter = iota - FilterLinear - FilterLinearMipmapLinear -) - -const ( - FeatureTimers Features = 1 << iota - FeatureFloatRenderTargets - FeatureCompute - FeatureSRGB -) - -const ( - TopologyTriangleStrip Topology = iota - TopologyTriangles -) - -const ( - BlendFactorOne BlendFactor = iota - BlendFactorOneMinusSrcAlpha - BlendFactorZero - BlendFactorDstColor -) - -const ( - LoadActionKeep LoadAction = iota - LoadActionClear - LoadActionInvalidate -) - -var ErrContentLost = errors.New("buffer content lost") - -func (f Features) Has(feats Features) bool { - return f&feats == feats -} - -func DownloadImage(d Device, t Texture, img *image.RGBA) error { - r := img.Bounds() - if err := t.ReadPixels(r, img.Pix, img.Stride); err != nil { - return err - } - if d.Caps().BottomLeftOrigin { - // OpenGL origin is in the lower-left corner. Flip the image to - // match. - flipImageY(r.Dx()*4, r.Dy(), img.Pix) - } - return nil -} - -func flipImageY(stride, height int, pixels []byte) { - // Flip image in y-direction. OpenGL's origin is in the lower - // left corner. - row := make([]uint8, stride) - for y := range height / 2 { - y1 := height - y - 1 - dest := y1 * stride - src := y * stride - copy(row, pixels[dest:]) - copy(pixels[dest:], pixels[src:src+len(row)]) - copy(pixels[src:], row) - } -} - -func UploadImage(t Texture, offset image.Point, img *image.RGBA) { - var pixels []byte - size := img.Bounds().Size() - min := img.Rect.Min - start := img.PixOffset(min.X, min.Y) - end := img.PixOffset(min.X+size.X, min.Y+size.Y-1) - pixels = img.Pix[start:end] - t.Upload(offset, size, pixels, img.Stride) -} diff --git a/gio/gpu/internal/metal/metal.go b/gio/gpu/internal/metal/metal.go deleted file mode 100644 index b9739af..0000000 --- a/gio/gpu/internal/metal/metal.go +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// This file exists so this package builds on non-Darwin platforms. - -package metal diff --git a/gio/gpu/internal/metal/metal_darwin.go b/gio/gpu/internal/metal/metal_darwin.go deleted file mode 100644 index 13a3c85..0000000 --- a/gio/gpu/internal/metal/metal_darwin.go +++ /dev/null @@ -1,1159 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package metal - -import ( - "errors" - "fmt" - "image" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" - "github.com/p9c/p9/pkg/gel/gio/shader" -) - -/* -#cgo CFLAGS: -Werror -xobjective-c -fobjc-arc -#cgo LDFLAGS: -framework CoreGraphics -framework Metal -framework Foundation - -#include -#include - -typedef struct { - void *addr; - NSUInteger size; -} slice; - -static CFTypeRef queueNewBuffer(CFTypeRef queueRef) { - @autoreleasepool { - id queue = (__bridge id)queueRef; - return CFBridgingRetain([queue commandBuffer]); - } -} - -static void cmdBufferCommit(CFTypeRef cmdBufRef) { - @autoreleasepool { - id cmdBuf = (__bridge id)cmdBufRef; - [cmdBuf commit]; - } -} - -static void cmdBufferWaitUntilCompleted(CFTypeRef cmdBufRef) { - @autoreleasepool { - id cmdBuf = (__bridge id)cmdBufRef; - [cmdBuf waitUntilCompleted]; - } -} - -static CFTypeRef cmdBufferRenderEncoder(CFTypeRef cmdBufRef, CFTypeRef textureRef, MTLLoadAction act, float r, float g, float b, float a) { - @autoreleasepool { - id cmdBuf = (__bridge id)cmdBufRef; - MTLRenderPassDescriptor *desc = [MTLRenderPassDescriptor new]; - desc.colorAttachments[0].texture = (__bridge id)textureRef; - desc.colorAttachments[0].loadAction = act; - desc.colorAttachments[0].clearColor = MTLClearColorMake(r, g, b, a); - return CFBridgingRetain([cmdBuf renderCommandEncoderWithDescriptor:desc]); - } -} - -static CFTypeRef cmdBufferComputeEncoder(CFTypeRef cmdBufRef) { - @autoreleasepool { - id cmdBuf = (__bridge id)cmdBufRef; - return CFBridgingRetain([cmdBuf computeCommandEncoder]); - } -} - -static CFTypeRef cmdBufferBlitEncoder(CFTypeRef cmdBufRef) { - @autoreleasepool { - id cmdBuf = (__bridge id)cmdBufRef; - return CFBridgingRetain([cmdBuf blitCommandEncoder]); - } -} - -static void renderEncEnd(CFTypeRef renderEncRef) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - [enc endEncoding]; - } -} - -static void renderEncViewport(CFTypeRef renderEncRef, MTLViewport viewport) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - [enc setViewport:viewport]; - } -} - -static void renderEncSetFragmentTexture(CFTypeRef renderEncRef, NSUInteger index, CFTypeRef texRef) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - id tex = (__bridge id)texRef; - [enc setFragmentTexture:tex atIndex:index]; - } -} - -static void renderEncSetFragmentSamplerState(CFTypeRef renderEncRef, NSUInteger index, CFTypeRef samplerRef) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - id sampler = (__bridge id)samplerRef; - [enc setFragmentSamplerState:sampler atIndex:index]; - } -} - -static void renderEncSetVertexBuffer(CFTypeRef renderEncRef, CFTypeRef bufRef, NSUInteger idx, NSUInteger offset) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - id buf = (__bridge id)bufRef; - [enc setVertexBuffer:buf offset:offset atIndex:idx]; - } -} - -static void renderEncSetFragmentBuffer(CFTypeRef renderEncRef, CFTypeRef bufRef, NSUInteger idx, NSUInteger offset) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - id buf = (__bridge id)bufRef; - [enc setFragmentBuffer:buf offset:offset atIndex:idx]; - } -} - -static void renderEncSetFragmentBytes(CFTypeRef renderEncRef, const void *bytes, NSUInteger length, NSUInteger idx) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - [enc setFragmentBytes:bytes length:length atIndex:idx]; - } -} - -static void renderEncSetVertexBytes(CFTypeRef renderEncRef, const void *bytes, NSUInteger length, NSUInteger idx) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - [enc setVertexBytes:bytes length:length atIndex:idx]; - } -} - -static void renderEncSetRenderPipelineState(CFTypeRef renderEncRef, CFTypeRef pipeRef) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - id pipe = (__bridge id)pipeRef; - [enc setRenderPipelineState:pipe]; - } -} - -static void renderEncDrawPrimitives(CFTypeRef renderEncRef, MTLPrimitiveType type, NSUInteger start, NSUInteger count) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - [enc drawPrimitives:type vertexStart:start vertexCount:count]; - } -} - -static void renderEncDrawIndexedPrimitives(CFTypeRef renderEncRef, MTLPrimitiveType type, CFTypeRef bufRef, NSUInteger offset, NSUInteger count) { - @autoreleasepool { - id enc = (__bridge id)renderEncRef; - id buf = (__bridge id)bufRef; - [enc drawIndexedPrimitives:type indexCount:count indexType:MTLIndexTypeUInt16 indexBuffer:buf indexBufferOffset:offset]; - } -} - -static void computeEncSetPipeline(CFTypeRef computeEncRef, CFTypeRef pipeRef) { - @autoreleasepool { - id enc = (__bridge id)computeEncRef; - id pipe = (__bridge id)pipeRef; - [enc setComputePipelineState:pipe]; - } -} - -static void computeEncSetTexture(CFTypeRef computeEncRef, NSUInteger index, CFTypeRef texRef) { - @autoreleasepool { - id enc = (__bridge id)computeEncRef; - id tex = (__bridge id)texRef; - [enc setTexture:tex atIndex:index]; - } -} - -static void computeEncEnd(CFTypeRef computeEncRef) { - @autoreleasepool { - id enc = (__bridge id)computeEncRef; - [enc endEncoding]; - } -} - -static void computeEncSetBuffer(CFTypeRef computeEncRef, NSUInteger index, CFTypeRef bufRef) { - @autoreleasepool { - id enc = (__bridge id)computeEncRef; - id buf = (__bridge id)bufRef; - [enc setBuffer:buf offset:0 atIndex:index]; - } -} - -static void computeEncDispatch(CFTypeRef computeEncRef, MTLSize threadgroupsPerGrid, MTLSize threadsPerThreadgroup) { - @autoreleasepool { - id enc = (__bridge id)computeEncRef; - [enc dispatchThreadgroups:threadgroupsPerGrid threadsPerThreadgroup:threadsPerThreadgroup]; - } -} - -static void computeEncSetBytes(CFTypeRef computeEncRef, const void *bytes, NSUInteger length, NSUInteger index) { - @autoreleasepool { - id enc = (__bridge id)computeEncRef; - [enc setBytes:bytes length:length atIndex:index]; - } -} - -static void blitEncEnd(CFTypeRef blitEncRef) { - @autoreleasepool { - id enc = (__bridge id)blitEncRef; - [enc endEncoding]; - } -} - -static void blitEncCopyFromTexture(CFTypeRef blitEncRef, CFTypeRef srcRef, MTLOrigin srcOrig, MTLSize srcSize, CFTypeRef dstRef, MTLOrigin dstOrig) { - @autoreleasepool { - id enc = (__bridge id)blitEncRef; - id src = (__bridge id)srcRef; - id dst = (__bridge id)dstRef; - [enc copyFromTexture:src - sourceSlice:0 - sourceLevel:0 - sourceOrigin:srcOrig - sourceSize:srcSize - toTexture:dst - destinationSlice:0 - destinationLevel:0 - destinationOrigin:dstOrig]; - } -} - -static void blitEncCopyBufferToTexture(CFTypeRef blitEncRef, CFTypeRef bufRef, CFTypeRef texRef, NSUInteger offset, NSUInteger stride, NSUInteger length, MTLSize dims, MTLOrigin orig) { - @autoreleasepool { - id enc = (__bridge id)blitEncRef; - id src = (__bridge id)bufRef; - id dst = (__bridge id)texRef; - [enc copyFromBuffer:src - sourceOffset:offset - sourceBytesPerRow:stride - sourceBytesPerImage:length - sourceSize:dims - toTexture:dst - destinationSlice:0 - destinationLevel:0 - destinationOrigin:orig]; - } -} - -static void blitEncGenerateMipmapsForTexture(CFTypeRef blitEncRef, CFTypeRef texRef) { - @autoreleasepool { - id enc = (__bridge id)blitEncRef; - id tex = (__bridge id)texRef; - [enc generateMipmapsForTexture: tex]; - } -} - -static void blitEncCopyTextureToBuffer(CFTypeRef blitEncRef, CFTypeRef texRef, CFTypeRef bufRef, NSUInteger offset, NSUInteger stride, NSUInteger length, MTLSize dims, MTLOrigin orig) { - @autoreleasepool { - id enc = (__bridge id)blitEncRef; - id src = (__bridge id)texRef; - id dst = (__bridge id)bufRef; - [enc copyFromTexture:src - sourceSlice:0 - sourceLevel:0 - sourceOrigin:orig - sourceSize:dims - toBuffer:dst - destinationOffset:offset - destinationBytesPerRow:stride - destinationBytesPerImage:length]; - } -} - -static void blitEncCopyBufferToBuffer(CFTypeRef blitEncRef, CFTypeRef srcRef, CFTypeRef dstRef, NSUInteger srcOff, NSUInteger dstOff, NSUInteger size) { - @autoreleasepool { - id enc = (__bridge id)blitEncRef; - id src = (__bridge id)srcRef; - id dst = (__bridge id)dstRef; - [enc copyFromBuffer:src - sourceOffset:srcOff - toBuffer:dst - destinationOffset:dstOff - size:size]; - } -} - -static CFTypeRef newTexture(CFTypeRef devRef, NSUInteger width, NSUInteger height, MTLPixelFormat format, MTLTextureUsage usage, int mipmapped) { - @autoreleasepool { - id dev = (__bridge id)devRef; - MTLTextureDescriptor *mtlDesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat: format - width: width - height: height - mipmapped: mipmapped ? YES : NO]; - mtlDesc.usage = usage; - mtlDesc.storageMode = MTLStorageModePrivate; - return CFBridgingRetain([dev newTextureWithDescriptor:mtlDesc]); - } -} - -static CFTypeRef newSampler(CFTypeRef devRef, MTLSamplerMinMagFilter minFilter, MTLSamplerMinMagFilter magFilter, MTLSamplerMipFilter mipFilter) { - @autoreleasepool { - id dev = (__bridge id)devRef; - MTLSamplerDescriptor *desc = [MTLSamplerDescriptor new]; - desc.minFilter = minFilter; - desc.magFilter = magFilter; - desc.mipFilter = mipFilter; - return CFBridgingRetain([dev newSamplerStateWithDescriptor:desc]); - } -} - -static CFTypeRef newBuffer(CFTypeRef devRef, NSUInteger size, MTLResourceOptions opts) { - @autoreleasepool { - id dev = (__bridge id)devRef; - id buf = [dev newBufferWithLength:size - options:opts]; - return CFBridgingRetain(buf); - } -} - -static slice bufferContents(CFTypeRef bufRef) { - @autoreleasepool { - id buf = (__bridge id)bufRef; - slice s = {.addr = [buf contents], .size = [buf length]}; - return s; - } -} - -static CFTypeRef newLibrary(CFTypeRef devRef, char *name, void *mtllib, size_t size) { - @autoreleasepool { - id dev = (__bridge id)devRef; - dispatch_data_t data = dispatch_data_create(mtllib, size, DISPATCH_TARGET_QUEUE_DEFAULT, DISPATCH_DATA_DESTRUCTOR_DEFAULT); - id lib = [dev newLibraryWithData:data error:nil]; - lib.label = [NSString stringWithUTF8String:name]; - return CFBridgingRetain(lib); - } -} - -static CFTypeRef libraryNewFunction(CFTypeRef libRef, char *funcName) { - @autoreleasepool { - id lib = (__bridge id)libRef; - NSString *name = [NSString stringWithUTF8String:funcName]; - return CFBridgingRetain([lib newFunctionWithName:name]); - } -} - -static CFTypeRef newComputePipeline(CFTypeRef devRef, CFTypeRef funcRef) { - @autoreleasepool { - id dev = (__bridge id)devRef; - id func = (__bridge id)funcRef; - return CFBridgingRetain([dev newComputePipelineStateWithFunction:func error:nil]); - } -} - -static CFTypeRef newRenderPipeline(CFTypeRef devRef, CFTypeRef vertFunc, CFTypeRef fragFunc, MTLPixelFormat pixelFormat, NSUInteger bufIdx, NSUInteger nverts, MTLVertexFormat *fmts, NSUInteger *offsets, NSUInteger stride, int blend, MTLBlendFactor srcFactor, MTLBlendFactor dstFactor, NSUInteger nvertBufs, NSUInteger nfragBufs) { - @autoreleasepool { - id dev = (__bridge id)devRef; - id vfunc = (__bridge id)vertFunc; - id ffunc = (__bridge id)fragFunc; - MTLVertexDescriptor *vdesc = [MTLVertexDescriptor vertexDescriptor]; - vdesc.layouts[bufIdx].stride = stride; - for (NSUInteger i = 0; i < nverts; i++) { - vdesc.attributes[i].format = fmts[i]; - vdesc.attributes[i].offset = offsets[i]; - vdesc.attributes[i].bufferIndex = bufIdx; - } - MTLRenderPipelineDescriptor *desc = [MTLRenderPipelineDescriptor new]; - desc.vertexFunction = vfunc; - desc.fragmentFunction = ffunc; - desc.vertexDescriptor = vdesc; - for (NSUInteger i = 0; i < nvertBufs; i++) { - if (@available(iOS 11.0, *)) { - desc.vertexBuffers[i].mutability = MTLMutabilityImmutable; - } - } - for (NSUInteger i = 0; i < nfragBufs; i++) { - if (@available(iOS 11.0, *)) { - desc.fragmentBuffers[i].mutability = MTLMutabilityImmutable; - } - } - desc.colorAttachments[0].pixelFormat = pixelFormat; - desc.colorAttachments[0].blendingEnabled = blend ? YES : NO; - desc.colorAttachments[0].sourceAlphaBlendFactor = srcFactor; - desc.colorAttachments[0].sourceRGBBlendFactor = srcFactor; - desc.colorAttachments[0].destinationAlphaBlendFactor = dstFactor; - desc.colorAttachments[0].destinationRGBBlendFactor = dstFactor; - return CFBridgingRetain([dev newRenderPipelineStateWithDescriptor:desc - error:nil]); - } -} -*/ -import "C" - -type Backend struct { - dev C.CFTypeRef - queue C.CFTypeRef - pixelFmt C.MTLPixelFormat - - cmdBuffer C.CFTypeRef - lastCmdBuffer C.CFTypeRef - renderEnc C.CFTypeRef - computeEnc C.CFTypeRef - blitEnc C.CFTypeRef - - prog *Program - topology C.MTLPrimitiveType - - stagingBuf C.CFTypeRef - stagingOff int - - indexBuf *Buffer - - // bufSizes is scratch space for filling out the spvBufferSizeConstants - // that spirv-cross generates for emulating buffer.length expressions in - // shaders. - bufSizes []uint32 -} - -type Texture struct { - backend *Backend - texture C.CFTypeRef - sampler C.CFTypeRef - width int - height int - mipmap bool - foreign bool -} - -type Shader struct { - function C.CFTypeRef - inputs []shader.InputLocation -} - -type Program struct { - pipeline C.CFTypeRef - groupSize [3]int -} - -type Pipeline struct { - pipeline C.CFTypeRef - topology C.MTLPrimitiveType -} - -type Buffer struct { - backend *Backend - size int - buffer C.CFTypeRef - - // store is the buffer contents For buffers not allocated on the GPU. - store []byte -} - -const ( - uniformBufferIndex = 0 - attributeBufferIndex = 1 - - spvBufferSizeConstantsBinding = 25 -) - -const ( - texUnits = 4 - bufferUnits = 4 -) - -func init() { - driver.NewMetalDevice = newMetalDevice -} - -func newMetalDevice(api driver.Metal) (driver.Device, error) { - dev := C.CFTypeRef(api.Device) - C.CFRetain(dev) - queue := C.CFTypeRef(api.Queue) - C.CFRetain(queue) - b := &Backend{ - dev: dev, - queue: queue, - pixelFmt: C.MTLPixelFormat(api.PixelFormat), - bufSizes: make([]uint32, bufferUnits), - } - return b, nil -} - -func (b *Backend) BeginFrame(target driver.RenderTarget, clear bool, viewport image.Point) driver.Texture { - if b.lastCmdBuffer != 0 { - C.cmdBufferWaitUntilCompleted(b.lastCmdBuffer) - b.stagingOff = 0 - } - if target == nil { - return nil - } - switch t := target.(type) { - case driver.MetalRenderTarget: - texture := C.CFTypeRef(t.Texture) - return &Texture{texture: texture, foreign: true} - case *Texture: - return t - default: - panic(fmt.Sprintf("metal: unsupported render target type: %T", t)) - } -} - -func (b *Backend) startBlit() C.CFTypeRef { - if b.blitEnc != 0 { - return b.blitEnc - } - b.endEncoder() - b.ensureCmdBuffer() - b.blitEnc = C.cmdBufferBlitEncoder(b.cmdBuffer) - if b.blitEnc == 0 { - panic("metal: [MTLCommandBuffer blitCommandEncoder:] failed") - } - return b.blitEnc -} - -func (b *Backend) CopyTexture(dst driver.Texture, dorig image.Point, src driver.Texture, srect image.Rectangle) { - enc := b.startBlit() - dstTex := dst.(*Texture).texture - srcTex := src.(*Texture).texture - ssz := srect.Size() - C.blitEncCopyFromTexture( - enc, - srcTex, - C.MTLOrigin{ - x: C.NSUInteger(srect.Min.X), - y: C.NSUInteger(srect.Min.Y), - }, - C.MTLSize{ - width: C.NSUInteger(ssz.X), - height: C.NSUInteger(ssz.Y), - depth: 1, - }, - dstTex, - C.MTLOrigin{ - x: C.NSUInteger(dorig.X), - y: C.NSUInteger(dorig.Y), - }, - ) -} - -func (b *Backend) EndFrame() { - b.endCmdBuffer(false) -} - -func (b *Backend) endCmdBuffer(wait bool) { - b.endEncoder() - if b.cmdBuffer == 0 { - return - } - C.cmdBufferCommit(b.cmdBuffer) - if wait { - C.cmdBufferWaitUntilCompleted(b.cmdBuffer) - } - if b.lastCmdBuffer != 0 { - C.CFRelease(b.lastCmdBuffer) - } - b.lastCmdBuffer = b.cmdBuffer - b.cmdBuffer = 0 -} - -func (b *Backend) Caps() driver.Caps { - return driver.Caps{ - MaxTextureSize: 8192, - Features: driver.FeatureSRGB | driver.FeatureCompute | driver.FeatureFloatRenderTargets, - } -} - -func (b *Backend) NewTimer() driver.Timer { - panic("timers not supported") -} - -func (b *Backend) IsTimeContinuous() bool { - panic("timers not supported") -} - -func (b *Backend) Release() { - if b.cmdBuffer != 0 { - C.CFRelease(b.cmdBuffer) - } - if b.lastCmdBuffer != 0 { - C.CFRelease(b.lastCmdBuffer) - } - if b.stagingBuf != 0 { - C.CFRelease(b.stagingBuf) - } - C.CFRelease(b.queue) - C.CFRelease(b.dev) - *b = Backend{} -} - -func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, minFilter, magFilter driver.TextureFilter, bindings driver.BufferBinding) (driver.Texture, error) { - mformat := pixelFormatFor(format) - var usage C.MTLTextureUsage - if bindings&(driver.BufferBindingTexture|driver.BufferBindingShaderStorageRead) != 0 { - usage |= C.MTLTextureUsageShaderRead - } - if bindings&driver.BufferBindingFramebuffer != 0 { - usage |= C.MTLTextureUsageRenderTarget - } - if bindings&driver.BufferBindingShaderStorageWrite != 0 { - usage |= C.MTLTextureUsageShaderWrite - } - min, mip := samplerFilterFor(minFilter) - max, _ := samplerFilterFor(magFilter) - mipmap := mip != C.MTLSamplerMipFilterNotMipmapped - mipmapped := C.int(0) - if mipmap { - mipmapped = 1 - } - tex := C.newTexture(b.dev, C.NSUInteger(width), C.NSUInteger(height), mformat, usage, mipmapped) - if tex == 0 { - return nil, errors.New("metal: [MTLDevice newTextureWithDescriptor:] failed") - } - s := C.newSampler(b.dev, min, max, mip) - if s == 0 { - C.CFRelease(tex) - return nil, errors.New("metal: [MTLDevice newSamplerStateWithDescriptor:] failed") - } - return &Texture{backend: b, texture: tex, sampler: s, width: width, height: height, mipmap: mipmap}, nil -} - -func samplerFilterFor(f driver.TextureFilter) (C.MTLSamplerMinMagFilter, C.MTLSamplerMipFilter) { - switch f { - case driver.FilterNearest: - return C.MTLSamplerMinMagFilterNearest, C.MTLSamplerMipFilterNotMipmapped - case driver.FilterLinear: - return C.MTLSamplerMinMagFilterLinear, C.MTLSamplerMipFilterNotMipmapped - case driver.FilterLinearMipmapLinear: - return C.MTLSamplerMinMagFilterLinear, C.MTLSamplerMipFilterLinear - default: - panic("invalid texture filter") - } -} - -func (b *Backend) NewPipeline(desc driver.PipelineDesc) (driver.Pipeline, error) { - vsh, fsh := desc.VertexShader.(*Shader), desc.FragmentShader.(*Shader) - layout := desc.VertexLayout.Inputs - if got, exp := len(layout), len(vsh.inputs); got != exp { - return nil, fmt.Errorf("metal: number of input descriptors (%d) doesn't match number of inputs (%d)", got, exp) - } - formats := make([]C.MTLVertexFormat, len(layout)) - offsets := make([]C.NSUInteger, len(layout)) - for i, inp := range layout { - index := vsh.inputs[i].Location - formats[index] = vertFormatFor(vsh.inputs[i]) - offsets[index] = C.NSUInteger(inp.Offset) - } - var ( - fmtPtr *C.MTLVertexFormat - offPtr *C.NSUInteger - ) - if len(layout) > 0 { - fmtPtr = &formats[0] - offPtr = &offsets[0] - } - srcFactor := blendFactorFor(desc.BlendDesc.SrcFactor) - dstFactor := blendFactorFor(desc.BlendDesc.DstFactor) - blend := C.int(0) - if desc.BlendDesc.Enable { - blend = 1 - } - pf := b.pixelFmt - if f := desc.PixelFormat; f != driver.TextureFormatOutput { - pf = pixelFormatFor(f) - } - pipe := C.newRenderPipeline( - b.dev, - vsh.function, - fsh.function, - pf, - attributeBufferIndex, - C.NSUInteger(len(layout)), fmtPtr, offPtr, - C.NSUInteger(desc.VertexLayout.Stride), - blend, srcFactor, dstFactor, - 2, // Number of vertex buffers. - 1, // Number of fragment buffers. - ) - if pipe == 0 { - return nil, errors.New("metal: pipeline construction failed") - } - return &Pipeline{pipeline: pipe, topology: primitiveFor(desc.Topology)}, nil -} - -func dataTypeSize(d shader.DataType) int { - switch d { - case shader.DataTypeFloat: - return 4 - default: - panic("unsupported data type") - } -} - -func blendFactorFor(f driver.BlendFactor) C.MTLBlendFactor { - switch f { - case driver.BlendFactorZero: - return C.MTLBlendFactorZero - case driver.BlendFactorOne: - return C.MTLBlendFactorOne - case driver.BlendFactorOneMinusSrcAlpha: - return C.MTLBlendFactorOneMinusSourceAlpha - case driver.BlendFactorDstColor: - return C.MTLBlendFactorDestinationColor - default: - panic("unsupported blend factor") - } -} - -func vertFormatFor(f shader.InputLocation) C.MTLVertexFormat { - t := f.Type - s := f.Size - switch { - case t == shader.DataTypeFloat && s == 1: - return C.MTLVertexFormatFloat - case t == shader.DataTypeFloat && s == 2: - return C.MTLVertexFormatFloat2 - case t == shader.DataTypeFloat && s == 3: - return C.MTLVertexFormatFloat3 - case t == shader.DataTypeFloat && s == 4: - return C.MTLVertexFormatFloat4 - default: - panic("unsupported data type") - } -} - -func pixelFormatFor(f driver.TextureFormat) C.MTLPixelFormat { - switch f { - case driver.TextureFormatFloat: - return C.MTLPixelFormatR16Float - case driver.TextureFormatRGBA8: - return C.MTLPixelFormatRGBA8Unorm - case driver.TextureFormatSRGBA: - return C.MTLPixelFormatRGBA8Unorm_sRGB - default: - panic("unsupported pixel format") - } -} - -func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, error) { - // Transfer buffer contents in command encoders on every use for - // smaller buffers. The advantage is that buffer re-use during a frame - // won't occur a GPU wait. - // We can't do this for buffers written to by the GPU and read by the client, - // and Metal doesn't require a buffer for indexed draws. - if size <= 4096 && typ&(driver.BufferBindingShaderStorageWrite|driver.BufferBindingIndices) == 0 { - return &Buffer{size: size, store: make([]byte, size)}, nil - } - buf := C.newBuffer(b.dev, C.NSUInteger(size), C.MTLResourceStorageModePrivate) - return &Buffer{backend: b, size: size, buffer: buf}, nil -} - -func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, data []byte) (driver.Buffer, error) { - buf, err := b.NewBuffer(typ, len(data)) - if err != nil { - return nil, err - } - buf.Upload(data) - return buf, nil -} - -func (b *Backend) NewComputeProgram(src shader.Sources) (driver.Program, error) { - sh, err := b.newShader(src) - if err != nil { - return nil, err - } - defer sh.Release() - pipe := C.newComputePipeline(b.dev, sh.function) - if pipe == 0 { - return nil, fmt.Errorf("metal: compute program %q load failed", src.Name) - } - return &Program{pipeline: pipe, groupSize: src.WorkgroupSize}, nil -} - -func (b *Backend) NewVertexShader(src shader.Sources) (driver.VertexShader, error) { - return b.newShader(src) -} - -func (b *Backend) NewFragmentShader(src shader.Sources) (driver.FragmentShader, error) { - return b.newShader(src) -} - -func (b *Backend) newShader(src shader.Sources) (*Shader, error) { - vsrc := []byte(src.MetalLib) - cname := C.CString(src.Name) - defer C.free(unsafe.Pointer(cname)) - vlib := C.newLibrary(b.dev, cname, unsafe.Pointer(&vsrc[0]), C.size_t(len(vsrc))) - if vlib == 0 { - return nil, fmt.Errorf("metal: vertex shader %q load failed", src.Name) - } - defer C.CFRelease(vlib) - funcName := C.CString("main0") - defer C.free(unsafe.Pointer(funcName)) - f := C.libraryNewFunction(vlib, funcName) - if f == 0 { - return nil, fmt.Errorf("metal: main function not found in %q", src.Name) - } - return &Shader{function: f, inputs: src.Inputs}, nil -} - -func (b *Backend) Viewport(x, y, width, height int) { - enc := b.renderEnc - if enc == 0 { - panic("no active render pass") - } - C.renderEncViewport(enc, C.MTLViewport{ - originX: C.double(x), - originY: C.double(y), - width: C.double(width), - height: C.double(height), - znear: 0.0, - zfar: 1.0, - }) -} - -func (b *Backend) DrawArrays(off, count int) { - enc := b.renderEnc - if enc == 0 { - panic("no active render pass") - } - C.renderEncDrawPrimitives(enc, b.topology, C.NSUInteger(off), C.NSUInteger(count)) -} - -func (b *Backend) DrawElements(off, count int) { - enc := b.renderEnc - if enc == 0 { - panic("no active render pass") - } - C.renderEncDrawIndexedPrimitives(enc, b.topology, b.indexBuf.buffer, C.NSUInteger(off), C.NSUInteger(count)) -} - -func primitiveFor(mode driver.Topology) C.MTLPrimitiveType { - switch mode { - case driver.TopologyTriangles: - return C.MTLPrimitiveTypeTriangle - case driver.TopologyTriangleStrip: - return C.MTLPrimitiveTypeTriangleStrip - default: - panic("metal: unknown draw mode") - } -} - -func (b *Backend) BindImageTexture(unit int, tex driver.Texture) { - b.BindTexture(unit, tex) -} - -func (b *Backend) BeginCompute() { - b.endEncoder() - b.ensureCmdBuffer() - for i := range b.bufSizes { - b.bufSizes[i] = 0 - } - b.computeEnc = C.cmdBufferComputeEncoder(b.cmdBuffer) - if b.computeEnc == 0 { - panic("metal: [MTLCommandBuffer computeCommandEncoder:] failed") - } -} - -func (b *Backend) EndCompute() { - if b.computeEnc == 0 { - panic("no active compute pass") - } - C.computeEncEnd(b.computeEnc) - C.CFRelease(b.computeEnc) - b.computeEnc = 0 -} - -func (b *Backend) DispatchCompute(x, y, z int) { - enc := b.computeEnc - if enc == 0 { - panic("no active compute pass") - } - C.computeEncSetBytes(enc, unsafe.Pointer(&b.bufSizes[0]), C.NSUInteger(len(b.bufSizes)*4), spvBufferSizeConstantsBinding) - threadgroupsPerGrid := C.MTLSize{ - width: C.NSUInteger(x), height: C.NSUInteger(y), depth: C.NSUInteger(z), - } - sz := b.prog.groupSize - threadsPerThreadgroup := C.MTLSize{ - width: C.NSUInteger(sz[0]), height: C.NSUInteger(sz[1]), depth: C.NSUInteger(sz[2]), - } - C.computeEncDispatch(enc, threadgroupsPerGrid, threadsPerThreadgroup) -} - -func (b *Backend) stagingBuffer(size int) (C.CFTypeRef, int) { - if b.stagingBuf == 0 || b.stagingOff+size > len(bufferStore(b.stagingBuf)) { - if b.stagingBuf != 0 { - C.CFRelease(b.stagingBuf) - } - cap := 2 * (b.stagingOff + size) - b.stagingBuf = C.newBuffer(b.dev, C.NSUInteger(cap), C.MTLResourceStorageModeShared|C.MTLResourceCPUCacheModeWriteCombined) - if b.stagingBuf == 0 { - panic(fmt.Errorf("metal: failed to allocate %d bytes of staging buffer", cap)) - } - b.stagingOff = 0 - } - off := b.stagingOff - b.stagingOff += size - return b.stagingBuf, off -} - -func (t *Texture) Upload(offset, size image.Point, pixels []byte, stride int) { - if len(pixels) == 0 { - return - } - if stride == 0 { - stride = size.X * 4 - } - dstStride := size.X * 4 - n := size.Y * dstStride - buf, off := t.backend.stagingBuffer(n) - store := bufferSlice(buf, off, n) - var srcOff, dstOff int - for y := 0; y < size.Y; y++ { - srcRow := pixels[srcOff : srcOff+dstStride] - dstRow := store[dstOff : dstOff+dstStride] - copy(dstRow, srcRow) - dstOff += dstStride - srcOff += stride - } - enc := t.backend.startBlit() - orig := C.MTLOrigin{ - x: C.NSUInteger(offset.X), - y: C.NSUInteger(offset.Y), - } - msize := C.MTLSize{ - width: C.NSUInteger(size.X), - height: C.NSUInteger(size.Y), - depth: 1, - } - C.blitEncCopyBufferToTexture(enc, buf, t.texture, C.NSUInteger(off), C.NSUInteger(dstStride), C.NSUInteger(len(store)), msize, orig) - if t.mipmap { - C.blitEncGenerateMipmapsForTexture(enc, t.texture) - } -} - -func (t *Texture) Release() { - if t.foreign { - panic("metal: release of external texture") - } - C.CFRelease(t.texture) - C.CFRelease(t.sampler) - *t = Texture{} -} - -func (p *Pipeline) Release() { - C.CFRelease(p.pipeline) - *p = Pipeline{} -} - -func (b *Backend) PrepareTexture(tex driver.Texture) {} - -func (b *Backend) BindTexture(unit int, tex driver.Texture) { - t := tex.(*Texture) - if enc := b.renderEnc; enc != 0 { - C.renderEncSetFragmentTexture(enc, C.NSUInteger(unit), t.texture) - C.renderEncSetFragmentSamplerState(enc, C.NSUInteger(unit), t.sampler) - } else if enc := b.computeEnc; enc != 0 { - C.computeEncSetTexture(enc, C.NSUInteger(unit), t.texture) - } else { - panic("no active render nor compute pass") - } -} - -func (b *Backend) ensureCmdBuffer() { - if b.cmdBuffer != 0 { - return - } - b.cmdBuffer = C.queueNewBuffer(b.queue) - if b.cmdBuffer == 0 { - panic("metal: [MTLCommandQueue cmdBuffer] failed") - } -} - -func (b *Backend) BindPipeline(pipe driver.Pipeline) { - p := pipe.(*Pipeline) - enc := b.renderEnc - if enc == 0 { - panic("no active render pass") - } - C.renderEncSetRenderPipelineState(enc, p.pipeline) - b.topology = p.topology -} - -func (b *Backend) BindProgram(prog driver.Program) { - enc := b.computeEnc - if enc == 0 { - panic("no active compute pass") - } - p := prog.(*Program) - C.computeEncSetPipeline(enc, p.pipeline) - b.prog = p -} - -func (s *Shader) Release() { - C.CFRelease(s.function) - *s = Shader{} -} - -func (p *Program) Release() { - C.CFRelease(p.pipeline) - *p = Program{} -} - -func (b *Backend) BindStorageBuffer(binding int, buffer driver.Buffer) { - buf := buffer.(*Buffer) - b.bufSizes[binding] = uint32(buf.size) - enc := b.computeEnc - if enc == 0 { - panic("no active compute pass") - } - if buf.buffer != 0 { - C.computeEncSetBuffer(enc, C.NSUInteger(binding), buf.buffer) - } else if buf.size > 0 { - C.computeEncSetBytes(enc, unsafe.Pointer(&buf.store[0]), C.NSUInteger(buf.size), C.NSUInteger(binding)) - } -} - -func (b *Backend) BindUniforms(buf driver.Buffer) { - bf := buf.(*Buffer) - enc := b.renderEnc - if enc == 0 { - panic("no active render pass") - } - if bf.buffer != 0 { - C.renderEncSetVertexBuffer(enc, bf.buffer, uniformBufferIndex, 0) - C.renderEncSetFragmentBuffer(enc, bf.buffer, uniformBufferIndex, 0) - } else if bf.size > 0 { - C.renderEncSetVertexBytes(enc, unsafe.Pointer(&bf.store[0]), C.NSUInteger(bf.size), uniformBufferIndex) - C.renderEncSetFragmentBytes(enc, unsafe.Pointer(&bf.store[0]), C.NSUInteger(bf.size), uniformBufferIndex) - } -} - -func (b *Backend) BindVertexBuffer(buf driver.Buffer, offset int) { - bf := buf.(*Buffer) - enc := b.renderEnc - if enc == 0 { - panic("no active render pass") - } - if bf.buffer != 0 { - C.renderEncSetVertexBuffer(enc, bf.buffer, attributeBufferIndex, C.NSUInteger(offset)) - } else if n := bf.size - offset; n > 0 { - C.renderEncSetVertexBytes(enc, unsafe.Pointer(&bf.store[offset]), C.NSUInteger(n), attributeBufferIndex) - } -} - -func (b *Backend) BindIndexBuffer(buf driver.Buffer) { - b.indexBuf = buf.(*Buffer) -} - -func (b *Buffer) Download(data []byte) error { - if len(data) > b.size { - panic(fmt.Errorf("len(data) (%d) larger than len(content) (%d)", len(data), b.size)) - } - buf, off := b.backend.stagingBuffer(len(data)) - enc := b.backend.startBlit() - C.blitEncCopyBufferToBuffer(enc, b.buffer, buf, 0, C.NSUInteger(off), C.NSUInteger(len(data))) - b.backend.endCmdBuffer(true) - store := bufferSlice(buf, off, len(data)) - copy(data, store) - return nil -} - -func (b *Buffer) Upload(data []byte) { - if len(data) > b.size { - panic(fmt.Errorf("len(data) (%d) larger than len(content) (%d)", len(data), b.size)) - } - if b.buffer == 0 { - copy(b.store, data) - return - } - buf, off := b.backend.stagingBuffer(len(data)) - store := bufferSlice(buf, off, len(data)) - copy(store, data) - enc := b.backend.startBlit() - C.blitEncCopyBufferToBuffer(enc, buf, b.buffer, C.NSUInteger(off), 0, C.NSUInteger(len(store))) -} - -func bufferStore(buf C.CFTypeRef) []byte { - contents := C.bufferContents(buf) - return (*(*[1 << 30]byte)(contents.addr))[:contents.size:contents.size] -} - -func bufferSlice(buf C.CFTypeRef, off, len int) []byte { - store := bufferStore(buf) - return store[off : off+len] -} - -func (b *Buffer) Release() { - if b.buffer != 0 { - C.CFRelease(b.buffer) - } - *b = Buffer{} -} - -func (t *Texture) ReadPixels(src image.Rectangle, pixels []byte, stride int) error { - if len(pixels) == 0 { - return nil - } - sz := src.Size() - orig := C.MTLOrigin{ - x: C.NSUInteger(src.Min.X), - y: C.NSUInteger(src.Min.Y), - } - msize := C.MTLSize{ - width: C.NSUInteger(sz.X), - height: C.NSUInteger(sz.Y), - depth: 1, - } - stageStride := sz.X * 4 - n := sz.Y * stageStride - buf, off := t.backend.stagingBuffer(n) - enc := t.backend.startBlit() - C.blitEncCopyTextureToBuffer(enc, t.texture, buf, C.NSUInteger(off), C.NSUInteger(stageStride), C.NSUInteger(n), msize, orig) - t.backend.endCmdBuffer(true) - store := bufferSlice(buf, off, n) - var srcOff, dstOff int - for y := 0; y < sz.Y; y++ { - dstRow := pixels[srcOff : srcOff+stageStride] - srcRow := store[dstOff : dstOff+stageStride] - copy(dstRow, srcRow) - dstOff += stageStride - srcOff += stride - } - return nil -} - -func (b *Backend) BeginRenderPass(tex driver.Texture, d driver.LoadDesc) { - b.endEncoder() - b.ensureCmdBuffer() - f := tex.(*Texture) - col := d.ClearColor - var act C.MTLLoadAction - switch d.Action { - case driver.LoadActionKeep: - act = C.MTLLoadActionLoad - case driver.LoadActionClear: - act = C.MTLLoadActionClear - case driver.LoadActionInvalidate: - act = C.MTLLoadActionDontCare - } - b.renderEnc = C.cmdBufferRenderEncoder(b.cmdBuffer, f.texture, act, C.float(col.R), C.float(col.G), C.float(col.B), C.float(col.A)) - if b.renderEnc == 0 { - panic("metal: [MTLCommandBuffer renderCommandEncoderWithDescriptor:] failed") - } -} - -func (b *Backend) EndRenderPass() { - if b.renderEnc == 0 { - panic("no active render pass") - } - C.renderEncEnd(b.renderEnc) - C.CFRelease(b.renderEnc) - b.renderEnc = 0 -} - -func (b *Backend) endEncoder() { - if b.renderEnc != 0 { - panic("active render pass") - } - if b.computeEnc != 0 { - panic("active compute pass") - } - if b.blitEnc != 0 { - C.blitEncEnd(b.blitEnc) - C.CFRelease(b.blitEnc) - b.blitEnc = 0 - } -} - -func (f *Texture) ImplementsRenderTarget() {} diff --git a/gio/gpu/internal/opengl/opengl.go b/gio/gpu/internal/opengl/opengl.go deleted file mode 100644 index cfe967f..0000000 --- a/gio/gpu/internal/opengl/opengl.go +++ /dev/null @@ -1,1374 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package opengl - -import ( - "errors" - "fmt" - "image" - "math/bits" - "runtime" - "slices" - "strings" - "time" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" - "github.com/p9c/p9/pkg/gel/gio/internal/gl" - "github.com/p9c/p9/pkg/gel/gio/shader" -) - -// Backend implements driver.Device. -type Backend struct { - funcs *gl.Functions - - clear bool - glstate glState - state state - savedState glState - sharedCtx bool - - glver [2]int - gles bool - feats driver.Caps - // floatTriple holds the settings for floating point - // textures. - floatTriple textureTriple - // Single channel alpha textures. - alphaTriple textureTriple - srgbaTriple textureTriple - storage [storageBindings]*buffer - - outputFBO gl.Framebuffer - sRGBFBO *SRGBFBO - - // vertArray is bound during a frame. We don't need it, but - // core desktop OpenGL profile 3.3 requires some array bound. - vertArray gl.VertexArray -} - -// State tracking. -type glState struct { - drawFBO gl.Framebuffer - readFBO gl.Framebuffer - vertAttribs [5]struct { - obj gl.Buffer - enabled bool - size int - typ gl.Enum - normalized bool - stride int - offset uintptr - } - prog gl.Program - texUnits struct { - active gl.Enum - binds [2]gl.Texture - } - arrayBuf gl.Buffer - elemBuf gl.Buffer - uniBuf gl.Buffer - uniBufs [2]gl.Buffer - storeBuf gl.Buffer - storeBufs [4]gl.Buffer - vertArray gl.VertexArray - srgb bool - blend struct { - enable bool - srcRGB, dstRGB gl.Enum - srcA, dstA gl.Enum - } - clearColor [4]float32 - viewport [4]int - unpack_row_length int - pack_row_length int -} - -type state struct { - pipeline *pipeline - buffer bufferBinding -} - -type bufferBinding struct { - obj gl.Buffer - offset int -} - -type timer struct { - funcs *gl.Functions - obj gl.Query -} - -type texture struct { - backend *Backend - obj gl.Texture - fbo gl.Framebuffer - hasFBO bool - triple textureTriple - width int - height int - mipmap bool - bindings driver.BufferBinding - foreign bool -} - -type pipeline struct { - prog *program - inputs []shader.InputLocation - layout driver.VertexLayout - blend driver.BlendDesc - topology driver.Topology -} - -type buffer struct { - backend *Backend - hasBuffer bool - obj gl.Buffer - typ driver.BufferBinding - size int - immutable bool - // For emulation of uniform buffers. - data []byte -} - -type glshader struct { - backend *Backend - obj gl.Shader - src shader.Sources -} - -type program struct { - backend *Backend - obj gl.Program - vertUniforms uniforms - fragUniforms uniforms -} - -type uniforms struct { - locs []uniformLocation - size int -} - -type uniformLocation struct { - uniform gl.Uniform - offset int - typ shader.DataType - size int -} - -// textureTriple holds the type settings for -// a TexImage2D call. -type textureTriple struct { - internalFormat gl.Enum - format gl.Enum - typ gl.Enum -} - -const ( - storageBindings = 32 -) - -func init() { - driver.NewOpenGLDevice = newOpenGLDevice -} - -// Supporting compute programs is theoretically possible with OpenGL ES 3.1. In -// practice, there are too many driver issues, especially on Android (e.g. -// Google Pixel, Samsung J2 are both broken i different ways). Disable support -// and rely on Vulkan for devices that support it, and the CPU fallback for -// devices that don't. -const brokenGLES31 = true - -func newOpenGLDevice(api driver.OpenGL) (driver.Device, error) { - f, err := gl.NewFunctions(api.Context, api.ES) - if err != nil { - return nil, err - } - exts := strings.Split(f.GetString(gl.EXTENSIONS), " ") - glVer := f.GetString(gl.VERSION) - ver, gles, err := gl.ParseGLVersion(glVer) - if err != nil { - return nil, err - } - floatTriple, ffboErr := floatTripleFor(f, ver, exts) - srgbaTriple, srgbErr := srgbaTripleFor(ver, exts) - gles31 := gles && (ver[0] > 3 || (ver[0] == 3 && ver[1] >= 1)) - b := &Backend{ - glver: ver, - gles: gles, - funcs: f, - floatTriple: floatTriple, - alphaTriple: alphaTripleFor(ver), - srgbaTriple: srgbaTriple, - sharedCtx: api.Shared, - } - b.feats.BottomLeftOrigin = true - if srgbErr == nil { - b.feats.Features |= driver.FeatureSRGB - } - if ffboErr == nil { - b.feats.Features |= driver.FeatureFloatRenderTargets - } - if gles31 && !brokenGLES31 { - b.feats.Features |= driver.FeatureCompute - } - if hasExtension(exts, "GL_EXT_disjoint_timer_query_webgl2") || hasExtension(exts, "GL_EXT_disjoint_timer_query") { - b.feats.Features |= driver.FeatureTimers - } - b.feats.MaxTextureSize = f.GetInteger(gl.MAX_TEXTURE_SIZE) - if !b.sharedCtx { - // We have exclusive access to the context, so query the GL state once - // instead of at each frame. - b.glstate = b.queryState() - } - return b, nil -} - -func (b *Backend) BeginFrame(target driver.RenderTarget, clear bool, viewport image.Point) driver.Texture { - b.clear = clear - if b.sharedCtx { - b.glstate = b.queryState() - b.savedState = b.glstate - } - b.state = state{} - var renderFBO gl.Framebuffer - if target != nil { - switch t := target.(type) { - case driver.OpenGLRenderTarget: - renderFBO = gl.Framebuffer(t) - case *texture: - renderFBO = t.ensureFBO() - default: - panic(fmt.Errorf("opengl: invalid render target type: %T", target)) - } - } - b.outputFBO = renderFBO - b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, renderFBO) - if b.gles { - // If the output framebuffer is not in the sRGB colorspace already, emulate it. - fbSRGB := false - if !b.gles || b.glver[0] > 2 { - var fbEncoding int - if !renderFBO.Valid() { - fbEncoding = b.funcs.GetFramebufferAttachmentParameteri(gl.FRAMEBUFFER, gl.BACK, gl.FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING) - } else { - fbEncoding = b.funcs.GetFramebufferAttachmentParameteri(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING) - } - fbSRGB = fbEncoding != gl.LINEAR - } - if !fbSRGB && viewport != (image.Point{}) { - if b.sRGBFBO == nil { - sfbo, err := NewSRGBFBO(b.funcs, &b.glstate) - if err != nil { - panic(err) - } - b.sRGBFBO = sfbo - } - if err := b.sRGBFBO.Refresh(viewport); err != nil { - panic(err) - } - renderFBO = b.sRGBFBO.Framebuffer() - } else if b.sRGBFBO != nil { - b.sRGBFBO.Release() - b.sRGBFBO = nil - } - } else { - b.glstate.set(b.funcs, gl.FRAMEBUFFER_SRGB, true) - if !b.vertArray.Valid() { - b.vertArray = b.funcs.CreateVertexArray() - } - b.glstate.bindVertexArray(b.funcs, b.vertArray) - } - b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, renderFBO) - if b.sRGBFBO != nil && !clear { - b.clearOutput(0, 0, 0, 0) - } - return &texture{backend: b, fbo: renderFBO, hasFBO: true, foreign: true} -} - -func (b *Backend) EndFrame() { - if b.sRGBFBO != nil { - b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, b.outputFBO) - if b.clear { - b.SetBlend(false) - } else { - b.BlendFunc(driver.BlendFactorOne, driver.BlendFactorOneMinusSrcAlpha) - b.SetBlend(true) - } - b.sRGBFBO.Blit() - } - if b.sharedCtx { - b.restoreState(b.savedState) - } else if runtime.GOOS == "android" { - // The Android emulator needs the output framebuffer to be current when - // eglSwapBuffers is called. - b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, b.outputFBO) - } -} - -func (b *Backend) queryState() glState { - s := glState{ - prog: gl.Program(b.funcs.GetBinding(gl.CURRENT_PROGRAM)), - arrayBuf: gl.Buffer(b.funcs.GetBinding(gl.ARRAY_BUFFER_BINDING)), - elemBuf: gl.Buffer(b.funcs.GetBinding(gl.ELEMENT_ARRAY_BUFFER_BINDING)), - drawFBO: gl.Framebuffer(b.funcs.GetBinding(gl.FRAMEBUFFER_BINDING)), - clearColor: b.funcs.GetFloat4(gl.COLOR_CLEAR_VALUE), - viewport: b.funcs.GetInteger4(gl.VIEWPORT), - } - if !b.gles || b.glver[0] > 2 { - s.unpack_row_length = b.funcs.GetInteger(gl.UNPACK_ROW_LENGTH) - s.pack_row_length = b.funcs.GetInteger(gl.PACK_ROW_LENGTH) - } - s.blend.enable = b.funcs.IsEnabled(gl.BLEND) - s.blend.srcRGB = gl.Enum(b.funcs.GetInteger(gl.BLEND_SRC_RGB)) - s.blend.dstRGB = gl.Enum(b.funcs.GetInteger(gl.BLEND_DST_RGB)) - s.blend.srcA = gl.Enum(b.funcs.GetInteger(gl.BLEND_SRC_ALPHA)) - s.blend.dstA = gl.Enum(b.funcs.GetInteger(gl.BLEND_DST_ALPHA)) - s.texUnits.active = gl.Enum(b.funcs.GetInteger(gl.ACTIVE_TEXTURE)) - if !b.gles { - s.srgb = b.funcs.IsEnabled(gl.FRAMEBUFFER_SRGB) - } - if !b.gles || b.glver[0] >= 3 { - s.vertArray = gl.VertexArray(b.funcs.GetBinding(gl.VERTEX_ARRAY_BINDING)) - s.readFBO = gl.Framebuffer(b.funcs.GetBinding(gl.READ_FRAMEBUFFER_BINDING)) - s.uniBuf = gl.Buffer(b.funcs.GetBinding(gl.UNIFORM_BUFFER_BINDING)) - for i := range s.uniBufs { - s.uniBufs[i] = gl.Buffer(b.funcs.GetBindingi(gl.UNIFORM_BUFFER_BINDING, i)) - } - } - if b.gles && (b.glver[0] > 3 || (b.glver[0] == 3 && b.glver[1] >= 1)) { - s.storeBuf = gl.Buffer(b.funcs.GetBinding(gl.SHADER_STORAGE_BUFFER_BINDING)) - for i := range s.storeBufs { - s.storeBufs[i] = gl.Buffer(b.funcs.GetBindingi(gl.SHADER_STORAGE_BUFFER_BINDING, i)) - } - } - active := s.texUnits.active - for i := range s.texUnits.binds { - s.activeTexture(b.funcs, gl.TEXTURE0+gl.Enum(i)) - s.texUnits.binds[i] = gl.Texture(b.funcs.GetBinding(gl.TEXTURE_BINDING_2D)) - } - s.activeTexture(b.funcs, active) - for i := range s.vertAttribs { - a := &s.vertAttribs[i] - a.enabled = b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED) != gl.FALSE - a.obj = gl.Buffer(b.funcs.GetVertexAttribBinding(i, gl.VERTEX_ATTRIB_ARRAY_ENABLED)) - a.size = b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_SIZE) - a.typ = gl.Enum(b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_TYPE)) - a.normalized = b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_NORMALIZED) != gl.FALSE - a.stride = b.funcs.GetVertexAttrib(i, gl.VERTEX_ATTRIB_ARRAY_STRIDE) - a.offset = b.funcs.GetVertexAttribPointer(i, gl.VERTEX_ATTRIB_ARRAY_POINTER) - } - return s -} - -func (b *Backend) restoreState(dst glState) { - src := &b.glstate - f := b.funcs - for i, unit := range dst.texUnits.binds { - src.bindTexture(f, i, unit) - } - src.activeTexture(f, dst.texUnits.active) - src.bindFramebuffer(f, gl.FRAMEBUFFER, dst.drawFBO) - src.bindFramebuffer(f, gl.READ_FRAMEBUFFER, dst.readFBO) - src.set(f, gl.BLEND, dst.blend.enable) - bf := dst.blend - src.setBlendFuncSeparate(f, bf.srcRGB, bf.dstRGB, bf.srcA, bf.dstA) - src.set(f, gl.FRAMEBUFFER_SRGB, dst.srgb) - src.bindVertexArray(f, dst.vertArray) - src.useProgram(f, dst.prog) - src.bindBuffer(f, gl.ELEMENT_ARRAY_BUFFER, dst.elemBuf) - for i, b := range dst.uniBufs { - src.bindBufferBase(f, gl.UNIFORM_BUFFER, i, b) - } - src.bindBuffer(f, gl.UNIFORM_BUFFER, dst.uniBuf) - for i, b := range dst.storeBufs { - src.bindBufferBase(f, gl.SHADER_STORAGE_BUFFER, i, b) - } - src.bindBuffer(f, gl.SHADER_STORAGE_BUFFER, dst.storeBuf) - col := dst.clearColor - src.setClearColor(f, col[0], col[1], col[2], col[3]) - for i, attr := range dst.vertAttribs { - src.setVertexAttribArray(f, i, attr.enabled) - src.vertexAttribPointer(f, attr.obj, i, attr.size, attr.typ, attr.normalized, attr.stride, int(attr.offset)) - } - src.bindBuffer(f, gl.ARRAY_BUFFER, dst.arrayBuf) - v := dst.viewport - src.setViewport(f, v[0], v[1], v[2], v[3]) - src.pixelStorei(f, gl.UNPACK_ROW_LENGTH, dst.unpack_row_length) - src.pixelStorei(f, gl.PACK_ROW_LENGTH, dst.pack_row_length) -} - -func (s *glState) setVertexAttribArray(f *gl.Functions, idx int, enabled bool) { - a := &s.vertAttribs[idx] - if enabled != a.enabled { - if enabled { - f.EnableVertexAttribArray(gl.Attrib(idx)) - } else { - f.DisableVertexAttribArray(gl.Attrib(idx)) - } - a.enabled = enabled - } -} - -func (s *glState) vertexAttribPointer(f *gl.Functions, buf gl.Buffer, idx, size int, typ gl.Enum, normalized bool, stride, offset int) { - s.bindBuffer(f, gl.ARRAY_BUFFER, buf) - a := &s.vertAttribs[idx] - a.obj = buf - a.size = size - a.typ = typ - a.normalized = normalized - a.stride = stride - a.offset = uintptr(offset) - f.VertexAttribPointer(gl.Attrib(idx), a.size, a.typ, a.normalized, a.stride, int(a.offset)) -} - -func (s *glState) activeTexture(f *gl.Functions, unit gl.Enum) { - if unit != s.texUnits.active { - f.ActiveTexture(unit) - s.texUnits.active = unit - } -} - -func (s *glState) bindTexture(f *gl.Functions, unit int, t gl.Texture) { - s.activeTexture(f, gl.TEXTURE0+gl.Enum(unit)) - if !t.Equal(s.texUnits.binds[unit]) { - f.BindTexture(gl.TEXTURE_2D, t) - s.texUnits.binds[unit] = t - } -} - -func (s *glState) bindVertexArray(f *gl.Functions, a gl.VertexArray) { - if !a.Equal(s.vertArray) { - f.BindVertexArray(a) - s.vertArray = a - } -} - -func (s *glState) deleteFramebuffer(f *gl.Functions, fbo gl.Framebuffer) { - f.DeleteFramebuffer(fbo) - if fbo.Equal(s.drawFBO) { - s.drawFBO = gl.Framebuffer{} - } - if fbo.Equal(s.readFBO) { - s.readFBO = gl.Framebuffer{} - } -} - -func (s *glState) deleteBuffer(f *gl.Functions, b gl.Buffer) { - f.DeleteBuffer(b) - if b.Equal(s.arrayBuf) { - s.arrayBuf = gl.Buffer{} - } - if b.Equal(s.elemBuf) { - s.elemBuf = gl.Buffer{} - } - if b.Equal(s.uniBuf) { - s.uniBuf = gl.Buffer{} - } - if b.Equal(s.storeBuf) { - s.uniBuf = gl.Buffer{} - } - for i, b2 := range s.storeBufs { - if b.Equal(b2) { - s.storeBufs[i] = gl.Buffer{} - } - } - for i, b2 := range s.uniBufs { - if b.Equal(b2) { - s.uniBufs[i] = gl.Buffer{} - } - } -} - -func (s *glState) deleteProgram(f *gl.Functions, p gl.Program) { - f.DeleteProgram(p) - if p.Equal(s.prog) { - s.prog = gl.Program{} - } -} - -func (s *glState) deleteVertexArray(f *gl.Functions, a gl.VertexArray) { - f.DeleteVertexArray(a) - if a.Equal(s.vertArray) { - s.vertArray = gl.VertexArray{} - } -} - -func (s *glState) deleteTexture(f *gl.Functions, t gl.Texture) { - f.DeleteTexture(t) - binds := &s.texUnits.binds - for i, obj := range binds { - if t.Equal(obj) { - binds[i] = gl.Texture{} - } - } -} - -func (s *glState) useProgram(f *gl.Functions, p gl.Program) { - if !p.Equal(s.prog) { - f.UseProgram(p) - s.prog = p - } -} - -func (s *glState) bindFramebuffer(f *gl.Functions, target gl.Enum, fbo gl.Framebuffer) { - switch target { - case gl.FRAMEBUFFER: - if fbo.Equal(s.drawFBO) && fbo.Equal(s.readFBO) { - return - } - s.drawFBO = fbo - s.readFBO = fbo - case gl.READ_FRAMEBUFFER: - if fbo.Equal(s.readFBO) { - return - } - s.readFBO = fbo - case gl.DRAW_FRAMEBUFFER: - if fbo.Equal(s.drawFBO) { - return - } - s.drawFBO = fbo - default: - panic("unknown target") - } - f.BindFramebuffer(target, fbo) -} - -func (s *glState) bindBufferBase(f *gl.Functions, target gl.Enum, idx int, buf gl.Buffer) { - switch target { - case gl.UNIFORM_BUFFER: - if buf.Equal(s.uniBuf) && buf.Equal(s.uniBufs[idx]) { - return - } - s.uniBuf = buf - s.uniBufs[idx] = buf - case gl.SHADER_STORAGE_BUFFER: - if buf.Equal(s.storeBuf) && buf.Equal(s.storeBufs[idx]) { - return - } - s.storeBuf = buf - s.storeBufs[idx] = buf - default: - panic("unknown buffer target") - } - f.BindBufferBase(target, idx, buf) -} - -func (s *glState) bindBuffer(f *gl.Functions, target gl.Enum, buf gl.Buffer) { - switch target { - case gl.ARRAY_BUFFER: - if buf.Equal(s.arrayBuf) { - return - } - s.arrayBuf = buf - case gl.ELEMENT_ARRAY_BUFFER: - if buf.Equal(s.elemBuf) { - return - } - s.elemBuf = buf - case gl.UNIFORM_BUFFER: - if buf.Equal(s.uniBuf) { - return - } - s.uniBuf = buf - case gl.SHADER_STORAGE_BUFFER: - if buf.Equal(s.storeBuf) { - return - } - s.storeBuf = buf - default: - panic("unknown buffer target") - } - f.BindBuffer(target, buf) -} - -func (s *glState) pixelStorei(f *gl.Functions, pname gl.Enum, val int) { - switch pname { - case gl.UNPACK_ROW_LENGTH: - if val == s.unpack_row_length { - return - } - s.unpack_row_length = val - case gl.PACK_ROW_LENGTH: - if val == s.pack_row_length { - return - } - s.pack_row_length = val - default: - panic("unsupported PixelStorei pname") - } - f.PixelStorei(pname, val) -} - -func (s *glState) setClearColor(f *gl.Functions, r, g, b, a float32) { - col := [4]float32{r, g, b, a} - if col != s.clearColor { - f.ClearColor(r, g, b, a) - s.clearColor = col - } -} - -func (s *glState) setViewport(f *gl.Functions, x, y, width, height int) { - view := [4]int{x, y, width, height} - if view != s.viewport { - f.Viewport(x, y, width, height) - s.viewport = view - } -} - -func (s *glState) setBlendFuncSeparate(f *gl.Functions, srcRGB, dstRGB, srcA, dstA gl.Enum) { - if srcRGB != s.blend.srcRGB || dstRGB != s.blend.dstRGB || srcA != s.blend.srcA || dstA != s.blend.dstA { - s.blend.srcRGB = srcRGB - s.blend.dstRGB = dstRGB - s.blend.srcA = srcA - s.blend.dstA = dstA - f.BlendFuncSeparate(srcA, dstA, srcA, dstA) - } -} - -func (s *glState) set(f *gl.Functions, target gl.Enum, enable bool) { - switch target { - case gl.FRAMEBUFFER_SRGB: - if s.srgb == enable { - return - } - s.srgb = enable - case gl.BLEND: - if enable == s.blend.enable { - return - } - s.blend.enable = enable - default: - panic("unknown enable") - } - if enable { - f.Enable(target) - } else { - f.Disable(target) - } -} - -func (b *Backend) Caps() driver.Caps { - return b.feats -} - -func (b *Backend) NewTimer() driver.Timer { - return &timer{ - funcs: b.funcs, - obj: b.funcs.CreateQuery(), - } -} - -func (b *Backend) IsTimeContinuous() bool { - return b.funcs.GetInteger(gl.GPU_DISJOINT_EXT) == gl.FALSE -} - -func (t *texture) ensureFBO() gl.Framebuffer { - if t.hasFBO { - return t.fbo - } - b := t.backend - oldFBO := b.glstate.drawFBO - defer func() { - b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, oldFBO) - }() - glErr(b.funcs) - fb := b.funcs.CreateFramebuffer() - b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, fb) - if err := glErr(b.funcs); err != nil { - b.funcs.DeleteFramebuffer(fb) - panic(err) - } - b.funcs.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, t.obj, 0) - if st := b.funcs.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { - b.funcs.DeleteFramebuffer(fb) - panic(fmt.Errorf("incomplete framebuffer, status = 0x%x, err = %d", st, b.funcs.GetError())) - } - t.fbo = fb - t.hasFBO = true - return fb -} - -func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, minFilter, magFilter driver.TextureFilter, binding driver.BufferBinding) (driver.Texture, error) { - glErr(b.funcs) - tex := &texture{backend: b, obj: b.funcs.CreateTexture(), width: width, height: height, bindings: binding} - switch format { - case driver.TextureFormatFloat: - tex.triple = b.floatTriple - case driver.TextureFormatSRGBA: - tex.triple = b.srgbaTriple - case driver.TextureFormatRGBA8: - tex.triple = textureTriple{gl.RGBA8, gl.RGBA, gl.UNSIGNED_BYTE} - default: - return nil, errors.New("unsupported texture format") - } - b.BindTexture(0, tex) - min, mipmap := toTexFilter(minFilter) - mag, _ := toTexFilter(magFilter) - if b.gles && b.glver[0] < 3 { - // OpenGL ES 2 only supports mipmaps for power-of-two textures. - mipmap = false - } - tex.mipmap = mipmap - b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, mag) - b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, min) - b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) - b.funcs.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) - if mipmap { - nmipmaps := 1 - if mipmap { - dim := max(height, width) - log2 := 32 - bits.LeadingZeros32(uint32(dim)) - 1 - nmipmaps = log2 + 1 - } - // Immutable textures are required for BindImageTexture, and can't hurt otherwise. - b.funcs.TexStorage2D(gl.TEXTURE_2D, nmipmaps, tex.triple.internalFormat, width, height) - } else { - b.funcs.TexImage2D(gl.TEXTURE_2D, 0, tex.triple.internalFormat, width, height, tex.triple.format, tex.triple.typ) - } - if err := glErr(b.funcs); err != nil { - tex.Release() - return nil, err - } - return tex, nil -} - -func (b *Backend) NewBuffer(typ driver.BufferBinding, size int) (driver.Buffer, error) { - glErr(b.funcs) - buf := &buffer{backend: b, typ: typ, size: size} - if typ&driver.BufferBindingUniforms != 0 { - if typ != driver.BufferBindingUniforms { - return nil, errors.New("uniforms buffers cannot be bound as anything else") - } - buf.data = make([]byte, size) - } - if typ&^driver.BufferBindingUniforms != 0 { - buf.hasBuffer = true - buf.obj = b.funcs.CreateBuffer() - if err := glErr(b.funcs); err != nil { - buf.Release() - return nil, err - } - firstBinding := firstBufferType(typ) - b.glstate.bindBuffer(b.funcs, firstBinding, buf.obj) - b.funcs.BufferData(firstBinding, size, gl.DYNAMIC_DRAW, nil) - } - return buf, nil -} - -func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, data []byte) (driver.Buffer, error) { - glErr(b.funcs) - obj := b.funcs.CreateBuffer() - buf := &buffer{backend: b, obj: obj, typ: typ, size: len(data), hasBuffer: true} - firstBinding := firstBufferType(typ) - b.glstate.bindBuffer(b.funcs, firstBinding, buf.obj) - b.funcs.BufferData(firstBinding, len(data), gl.STATIC_DRAW, data) - buf.immutable = true - if err := glErr(b.funcs); err != nil { - buf.Release() - return nil, err - } - return buf, nil -} - -func glErr(f *gl.Functions) error { - if st := f.GetError(); st != gl.NO_ERROR { - return fmt.Errorf("glGetError: %#x", st) - } - return nil -} - -func (b *Backend) Release() { - if b.sRGBFBO != nil { - b.sRGBFBO.Release() - } - if b.vertArray.Valid() { - b.glstate.deleteVertexArray(b.funcs, b.vertArray) - } - *b = Backend{} -} - -func (b *Backend) DispatchCompute(x, y, z int) { - for binding, buf := range b.storage { - if buf != nil { - b.glstate.bindBufferBase(b.funcs, gl.SHADER_STORAGE_BUFFER, binding, buf.obj) - } - } - b.funcs.DispatchCompute(x, y, z) - b.funcs.MemoryBarrier(gl.ALL_BARRIER_BITS) -} - -func (b *Backend) BindImageTexture(unit int, tex driver.Texture) { - t := tex.(*texture) - var acc gl.Enum - switch t.bindings & (driver.BufferBindingShaderStorageRead | driver.BufferBindingShaderStorageWrite) { - case driver.BufferBindingShaderStorageRead: - acc = gl.READ_ONLY - case driver.BufferBindingShaderStorageWrite: - acc = gl.WRITE_ONLY - case driver.BufferBindingShaderStorageRead | driver.BufferBindingShaderStorageWrite: - acc = gl.READ_WRITE - default: - panic("unsupported access bits") - } - b.funcs.BindImageTexture(unit, t.obj, 0, false, 0, acc, t.triple.internalFormat) -} - -func (b *Backend) BlendFunc(sfactor, dfactor driver.BlendFactor) { - src, dst := toGLBlendFactor(sfactor), toGLBlendFactor(dfactor) - b.glstate.setBlendFuncSeparate(b.funcs, src, dst, src, dst) -} - -func toGLBlendFactor(f driver.BlendFactor) gl.Enum { - switch f { - case driver.BlendFactorOne: - return gl.ONE - case driver.BlendFactorOneMinusSrcAlpha: - return gl.ONE_MINUS_SRC_ALPHA - case driver.BlendFactorZero: - return gl.ZERO - case driver.BlendFactorDstColor: - return gl.DST_COLOR - default: - panic("unsupported blend factor") - } -} - -func (b *Backend) SetBlend(enable bool) { - b.glstate.set(b.funcs, gl.BLEND, enable) -} - -func (b *Backend) DrawElements(off, count int) { - b.prepareDraw() - // off is in 16-bit indices, but DrawElements take a byte offset. - byteOff := off * 2 - b.funcs.DrawElements(toGLDrawMode(b.state.pipeline.topology), count, gl.UNSIGNED_SHORT, byteOff) -} - -func (b *Backend) DrawArrays(off, count int) { - b.prepareDraw() - b.funcs.DrawArrays(toGLDrawMode(b.state.pipeline.topology), off, count) -} - -func (b *Backend) prepareDraw() { - p := b.state.pipeline - if p == nil { - return - } - b.setupVertexArrays() -} - -func toGLDrawMode(mode driver.Topology) gl.Enum { - switch mode { - case driver.TopologyTriangleStrip: - return gl.TRIANGLE_STRIP - case driver.TopologyTriangles: - return gl.TRIANGLES - default: - panic("unsupported draw mode") - } -} - -func (b *Backend) Viewport(x, y, width, height int) { - b.glstate.setViewport(b.funcs, x, y, width, height) -} - -func (b *Backend) clearOutput(colR, colG, colB, colA float32) { - b.glstate.setClearColor(b.funcs, colR, colG, colB, colA) - b.funcs.Clear(gl.COLOR_BUFFER_BIT) -} - -func (b *Backend) NewComputeProgram(src shader.Sources) (driver.Program, error) { - // We don't support ES 3.1 compute, see brokenGLES31 above. - const GLES31Source = "" - p, err := gl.CreateComputeProgram(b.funcs, GLES31Source) - if err != nil { - return nil, fmt.Errorf("%s: %v", src.Name, err) - } - return &program{ - backend: b, - obj: p, - }, nil -} - -func (b *Backend) NewVertexShader(src shader.Sources) (driver.VertexShader, error) { - glslSrc := b.glslFor(src) - sh, err := gl.CreateShader(b.funcs, gl.VERTEX_SHADER, glslSrc) - return &glshader{backend: b, obj: sh, src: src}, err -} - -func (b *Backend) NewFragmentShader(src shader.Sources) (driver.FragmentShader, error) { - glslSrc := b.glslFor(src) - sh, err := gl.CreateShader(b.funcs, gl.FRAGMENT_SHADER, glslSrc) - return &glshader{backend: b, obj: sh, src: src}, err -} - -func (b *Backend) glslFor(src shader.Sources) string { - if b.gles { - return src.GLSL100ES - } else { - return src.GLSL150 - } -} - -func (b *Backend) NewPipeline(desc driver.PipelineDesc) (driver.Pipeline, error) { - p, err := b.newProgram(desc) - if err != nil { - return nil, err - } - layout := desc.VertexLayout - vsrc := desc.VertexShader.(*glshader).src - if len(vsrc.Inputs) != len(layout.Inputs) { - return nil, fmt.Errorf("opengl: got %d inputs, expected %d", len(layout.Inputs), len(vsrc.Inputs)) - } - for i, inp := range vsrc.Inputs { - if exp, got := inp.Size, layout.Inputs[i].Size; exp != got { - return nil, fmt.Errorf("opengl: data size mismatch for %q: got %d expected %d", inp.Name, got, exp) - } - } - return &pipeline{ - prog: p, - inputs: vsrc.Inputs, - layout: layout, - blend: desc.BlendDesc, - topology: desc.Topology, - }, nil -} - -func (b *Backend) newProgram(desc driver.PipelineDesc) (*program, error) { - p := b.funcs.CreateProgram() - if !p.Valid() { - return nil, errors.New("opengl: glCreateProgram failed") - } - vsh, fsh := desc.VertexShader.(*glshader), desc.FragmentShader.(*glshader) - b.funcs.AttachShader(p, vsh.obj) - b.funcs.AttachShader(p, fsh.obj) - for _, inp := range vsh.src.Inputs { - b.funcs.BindAttribLocation(p, gl.Attrib(inp.Location), inp.Name) - } - b.funcs.LinkProgram(p) - if b.funcs.GetProgrami(p, gl.LINK_STATUS) == 0 { - log := b.funcs.GetProgramInfoLog(p) - b.funcs.DeleteProgram(p) - return nil, fmt.Errorf("opengl: program link failed: %s", strings.TrimSpace(log)) - } - prog := &program{ - backend: b, - obj: p, - } - b.glstate.useProgram(b.funcs, p) - // Bind texture uniforms. - for _, tex := range vsh.src.Textures { - u := b.funcs.GetUniformLocation(p, tex.Name) - if u.Valid() { - b.funcs.Uniform1i(u, tex.Binding) - } - } - for _, tex := range fsh.src.Textures { - u := b.funcs.GetUniformLocation(p, tex.Name) - if u.Valid() { - b.funcs.Uniform1i(u, tex.Binding) - } - } - prog.vertUniforms.setup(b.funcs, p, vsh.src.Uniforms.Size, vsh.src.Uniforms.Locations) - prog.fragUniforms.setup(b.funcs, p, fsh.src.Uniforms.Size, fsh.src.Uniforms.Locations) - return prog, nil -} - -func (b *Backend) BindStorageBuffer(binding int, buf driver.Buffer) { - bf := buf.(*buffer) - if bf.typ&(driver.BufferBindingShaderStorageRead|driver.BufferBindingShaderStorageWrite) == 0 { - panic("not a shader storage buffer") - } - b.storage[binding] = bf -} - -func (b *Backend) BindUniforms(buf driver.Buffer) { - bf := buf.(*buffer) - if bf.typ&driver.BufferBindingUniforms == 0 { - panic("not a uniform buffer") - } - b.state.pipeline.prog.vertUniforms.update(b.funcs, bf) - b.state.pipeline.prog.fragUniforms.update(b.funcs, bf) -} - -func (b *Backend) BindProgram(prog driver.Program) { - p := prog.(*program) - b.glstate.useProgram(b.funcs, p.obj) -} - -func (s *glshader) Release() { - s.backend.funcs.DeleteShader(s.obj) -} - -func (p *program) Release() { - p.backend.glstate.deleteProgram(p.backend.funcs, p.obj) -} - -func (u *uniforms) setup(funcs *gl.Functions, p gl.Program, uniformSize int, uniforms []shader.UniformLocation) { - u.locs = make([]uniformLocation, len(uniforms)) - for i, uniform := range uniforms { - loc := funcs.GetUniformLocation(p, uniform.Name) - u.locs[i] = uniformLocation{uniform: loc, offset: uniform.Offset, typ: uniform.Type, size: uniform.Size} - } - u.size = uniformSize -} - -func (p *uniforms) update(funcs *gl.Functions, buf *buffer) { - if buf.size < p.size { - panic(fmt.Errorf("uniform buffer too small, got %d need %d", buf.size, p.size)) - } - data := buf.data - for _, u := range p.locs { - if !u.uniform.Valid() { - continue - } - data := data[u.offset:] - switch { - case u.typ == shader.DataTypeFloat && u.size == 1: - data := data[:4] - v := *(*[1]float32)(unsafe.Pointer(&data[0])) - funcs.Uniform1f(u.uniform, v[0]) - case u.typ == shader.DataTypeFloat && u.size == 2: - data := data[:8] - v := *(*[2]float32)(unsafe.Pointer(&data[0])) - funcs.Uniform2f(u.uniform, v[0], v[1]) - case u.typ == shader.DataTypeFloat && u.size == 3: - data := data[:12] - v := *(*[3]float32)(unsafe.Pointer(&data[0])) - funcs.Uniform3f(u.uniform, v[0], v[1], v[2]) - case u.typ == shader.DataTypeFloat && u.size == 4: - data := data[:16] - v := *(*[4]float32)(unsafe.Pointer(&data[0])) - funcs.Uniform4f(u.uniform, v[0], v[1], v[2], v[3]) - default: - panic("unsupported uniform data type or size") - } - } -} - -func (b *buffer) Upload(data []byte) { - if b.immutable { - panic("immutable buffer") - } - if len(data) > b.size { - panic("buffer size overflow") - } - copy(b.data, data) - if b.hasBuffer { - firstBinding := firstBufferType(b.typ) - b.backend.glstate.bindBuffer(b.backend.funcs, firstBinding, b.obj) - if len(data) == b.size { - // the iOS GL implementation doesn't recognize when BufferSubData - // clears the entire buffer. Tell it and avoid GPU stalls. - // See also https://github.com/godotengine/godot/issues/23956. - b.backend.funcs.BufferData(firstBinding, b.size, gl.DYNAMIC_DRAW, data) - } else { - b.backend.funcs.BufferSubData(firstBinding, 0, data) - } - } -} - -func (b *buffer) Download(data []byte) error { - if len(data) > b.size { - panic("buffer size overflow") - } - if !b.hasBuffer { - copy(data, b.data) - return nil - } - firstBinding := firstBufferType(b.typ) - b.backend.glstate.bindBuffer(b.backend.funcs, firstBinding, b.obj) - bufferMap := b.backend.funcs.MapBufferRange(firstBinding, 0, len(data), gl.MAP_READ_BIT) - if bufferMap == nil { - return fmt.Errorf("MapBufferRange: error %#x", b.backend.funcs.GetError()) - } - copy(data, bufferMap) - if !b.backend.funcs.UnmapBuffer(firstBinding) { - return driver.ErrContentLost - } - return nil -} - -func (b *buffer) Release() { - if b.hasBuffer { - b.backend.glstate.deleteBuffer(b.backend.funcs, b.obj) - b.hasBuffer = false - } -} - -func (b *Backend) BindVertexBuffer(buf driver.Buffer, offset int) { - gbuf := buf.(*buffer) - if gbuf.typ&driver.BufferBindingVertices == 0 { - panic("not a vertex buffer") - } - b.state.buffer = bufferBinding{obj: gbuf.obj, offset: offset} -} - -func (b *Backend) setupVertexArrays() { - p := b.state.pipeline - inputs := p.inputs - if len(inputs) == 0 { - return - } - layout := p.layout - const max = len(b.glstate.vertAttribs) - var enabled [max]bool - buf := b.state.buffer - for i, inp := range inputs { - l := layout.Inputs[i] - var gltyp gl.Enum - switch l.Type { - case shader.DataTypeFloat: - gltyp = gl.FLOAT - case shader.DataTypeShort: - gltyp = gl.SHORT - default: - panic("unsupported data type") - } - enabled[inp.Location] = true - b.glstate.vertexAttribPointer(b.funcs, buf.obj, inp.Location, l.Size, gltyp, false, p.layout.Stride, buf.offset+l.Offset) - } - for i := range max { - b.glstate.setVertexAttribArray(b.funcs, i, enabled[i]) - } -} - -func (b *Backend) BindIndexBuffer(buf driver.Buffer) { - gbuf := buf.(*buffer) - if gbuf.typ&driver.BufferBindingIndices == 0 { - panic("not an index buffer") - } - b.glstate.bindBuffer(b.funcs, gl.ELEMENT_ARRAY_BUFFER, gbuf.obj) -} - -func (b *Backend) CopyTexture(dst driver.Texture, dstOrigin image.Point, src driver.Texture, srcRect image.Rectangle) { - const unit = 0 - oldTex := b.glstate.texUnits.binds[unit] - defer func() { - b.glstate.bindTexture(b.funcs, unit, oldTex) - }() - b.glstate.bindTexture(b.funcs, unit, dst.(*texture).obj) - b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, src.(*texture).ensureFBO()) - sz := srcRect.Size() - b.funcs.CopyTexSubImage2D(gl.TEXTURE_2D, 0, dstOrigin.X, dstOrigin.Y, srcRect.Min.X, srcRect.Min.Y, sz.X, sz.Y) -} - -func (t *texture) ReadPixels(src image.Rectangle, pixels []byte, stride int) error { - glErr(t.backend.funcs) - t.backend.glstate.bindFramebuffer(t.backend.funcs, gl.FRAMEBUFFER, t.ensureFBO()) - w, h := src.Dx(), src.Dy() - if len(pixels) < w*h*4 { - return errors.New("unexpected RGBA size") - } - // OpenGL ES 2 doesn't support PACK_ROW_LENGTH != 0. Avoid it if possible. - rowLen := 0 - if n := stride / 4; n != w { - rowLen = n - } - if rowLen == 0 || t.backend.glver[0] > 2 { - t.backend.glstate.pixelStorei(t.backend.funcs, gl.PACK_ROW_LENGTH, rowLen) - t.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels) - } else { - tmp := make([]byte, w*h*4) - t.backend.funcs.ReadPixels(src.Min.X, src.Min.Y, w, h, gl.RGBA, gl.UNSIGNED_BYTE, tmp) - for y := range h { - copy(pixels[y*stride:], tmp[y*w*4:]) - } - } - return glErr(t.backend.funcs) -} - -func (b *Backend) BindPipeline(pl driver.Pipeline) { - p := pl.(*pipeline) - b.state.pipeline = p - b.glstate.useProgram(b.funcs, p.prog.obj) - b.SetBlend(p.blend.Enable) - b.BlendFunc(p.blend.SrcFactor, p.blend.DstFactor) -} - -func (b *Backend) BeginCompute() { - b.funcs.MemoryBarrier(gl.ALL_BARRIER_BITS) -} - -func (b *Backend) EndCompute() { -} - -func (b *Backend) BeginRenderPass(tex driver.Texture, desc driver.LoadDesc) { - fbo := tex.(*texture).ensureFBO() - b.glstate.bindFramebuffer(b.funcs, gl.FRAMEBUFFER, fbo) - switch desc.Action { - case driver.LoadActionClear: - c := desc.ClearColor - b.clearOutput(c.R, c.G, c.B, c.A) - case driver.LoadActionInvalidate: - b.funcs.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) - } -} - -func (b *Backend) EndRenderPass() { -} - -func (f *texture) ImplementsRenderTarget() {} - -func (p *pipeline) Release() { - p.prog.Release() - *p = pipeline{} -} - -func toTexFilter(f driver.TextureFilter) (int, bool) { - switch f { - case driver.FilterNearest: - return gl.NEAREST, false - case driver.FilterLinear: - return gl.LINEAR, false - case driver.FilterLinearMipmapLinear: - return gl.LINEAR_MIPMAP_LINEAR, true - default: - panic("unsupported texture filter") - } -} - -func (b *Backend) PrepareTexture(tex driver.Texture) {} - -func (b *Backend) BindTexture(unit int, t driver.Texture) { - b.glstate.bindTexture(b.funcs, unit, t.(*texture).obj) -} - -func (t *texture) Release() { - if t.foreign { - panic("texture not created by NewTexture") - } - if t.hasFBO { - t.backend.glstate.deleteFramebuffer(t.backend.funcs, t.fbo) - } - t.backend.glstate.deleteTexture(t.backend.funcs, t.obj) -} - -func (t *texture) Upload(offset, size image.Point, pixels []byte, stride int) { - if min := size.X * size.Y * 4; min > len(pixels) { - panic(fmt.Errorf("size %d larger than data %d", min, len(pixels))) - } - t.backend.BindTexture(0, t) - // WebGL 1 doesn't support UNPACK_ROW_LENGTH != 0. Avoid it if possible. - rowLen := 0 - if n := stride / 4; n != size.X { - rowLen = n - } - t.backend.glstate.pixelStorei(t.backend.funcs, gl.UNPACK_ROW_LENGTH, rowLen) - t.backend.funcs.TexSubImage2D(gl.TEXTURE_2D, 0, offset.X, offset.Y, size.X, size.Y, t.triple.format, t.triple.typ, pixels) - if t.mipmap { - t.backend.funcs.GenerateMipmap(gl.TEXTURE_2D) - } -} - -func (t *timer) Begin() { - t.funcs.BeginQuery(gl.TIME_ELAPSED_EXT, t.obj) -} - -func (t *timer) End() { - t.funcs.EndQuery(gl.TIME_ELAPSED_EXT) -} - -func (t *timer) ready() bool { - return t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT_AVAILABLE) == gl.TRUE -} - -func (t *timer) Release() { - t.funcs.DeleteQuery(t.obj) -} - -func (t *timer) Duration() (time.Duration, bool) { - if !t.ready() { - return 0, false - } - nanos := t.funcs.GetQueryObjectuiv(t.obj, gl.QUERY_RESULT) - return time.Duration(nanos), true -} - -// floatTripleFor determines the best texture triple for floating point FBOs. -func floatTripleFor(f *gl.Functions, ver [2]int, exts []string) (textureTriple, error) { - var triples []textureTriple - if ver[0] >= 3 { - triples = append(triples, textureTriple{gl.R16F, gl.Enum(gl.RED), gl.Enum(gl.HALF_FLOAT)}) - } - // According to the OES_texture_half_float specification, EXT_color_buffer_half_float is needed to - // render to FBOs. However, the Safari WebGL1 implementation does support half-float FBOs but does not - // report EXT_color_buffer_half_float support. The triples are verified below, so it doesn't matter if we're - // wrong. - if hasExtension(exts, "GL_OES_texture_half_float") || hasExtension(exts, "GL_EXT_color_buffer_half_float") { - // Try single channel. - triples = append(triples, textureTriple{gl.LUMINANCE, gl.Enum(gl.LUMINANCE), gl.Enum(gl.HALF_FLOAT_OES)}) - // Fallback to 4 channels. - triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.HALF_FLOAT_OES)}) - } - if hasExtension(exts, "GL_OES_texture_float") || hasExtension(exts, "GL_EXT_color_buffer_float") { - triples = append(triples, textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.FLOAT)}) - } - tex := f.CreateTexture() - defer f.DeleteTexture(tex) - defTex := gl.Texture(f.GetBinding(gl.TEXTURE_BINDING_2D)) - defer f.BindTexture(gl.TEXTURE_2D, defTex) - f.BindTexture(gl.TEXTURE_2D, tex) - f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) - f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) - f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) - f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) - fbo := f.CreateFramebuffer() - defer f.DeleteFramebuffer(fbo) - defFBO := gl.Framebuffer(f.GetBinding(gl.FRAMEBUFFER_BINDING)) - f.BindFramebuffer(gl.FRAMEBUFFER, fbo) - defer f.BindFramebuffer(gl.FRAMEBUFFER, defFBO) - var attempts []string - for _, tt := range triples { - const size = 256 - f.TexImage2D(gl.TEXTURE_2D, 0, tt.internalFormat, size, size, tt.format, tt.typ) - f.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0) - st := f.CheckFramebufferStatus(gl.FRAMEBUFFER) - if st == gl.FRAMEBUFFER_COMPLETE { - return tt, nil - } - attempts = append(attempts, fmt.Sprintf("(0x%x, 0x%x, 0x%x): 0x%x", tt.internalFormat, tt.format, tt.typ, st)) - } - return textureTriple{}, fmt.Errorf("floating point fbos not supported (attempted %s)", attempts) -} - -func srgbaTripleFor(ver [2]int, exts []string) (textureTriple, error) { - switch { - case ver[0] >= 3: - return textureTriple{gl.SRGB8_ALPHA8, gl.Enum(gl.RGBA), gl.Enum(gl.UNSIGNED_BYTE)}, nil - case hasExtension(exts, "GL_EXT_sRGB"): - return textureTriple{gl.SRGB_ALPHA_EXT, gl.Enum(gl.SRGB_ALPHA_EXT), gl.Enum(gl.UNSIGNED_BYTE)}, nil - default: - return textureTriple{}, errors.New("no sRGB texture formats found") - } -} - -func alphaTripleFor(ver [2]int) textureTriple { - intf, f := gl.Enum(gl.R8), gl.Enum(gl.RED) - if ver[0] < 3 { - // R8, RED not supported on OpenGL ES 2.0. - intf, f = gl.LUMINANCE, gl.Enum(gl.LUMINANCE) - } - return textureTriple{intf, f, gl.UNSIGNED_BYTE} -} - -func hasExtension(exts []string, ext string) bool { - return slices.Contains(exts, ext) -} - -func firstBufferType(typ driver.BufferBinding) gl.Enum { - switch { - case typ&driver.BufferBindingIndices != 0: - return gl.ELEMENT_ARRAY_BUFFER - case typ&driver.BufferBindingVertices != 0: - return gl.ARRAY_BUFFER - case typ&driver.BufferBindingUniforms != 0: - return gl.UNIFORM_BUFFER - case typ&(driver.BufferBindingShaderStorageRead|driver.BufferBindingShaderStorageWrite) != 0: - return gl.SHADER_STORAGE_BUFFER - default: - panic("unsupported buffer type") - } -} diff --git a/gio/gpu/internal/opengl/srgb.go b/gio/gpu/internal/opengl/srgb.go deleted file mode 100644 index f30e092..0000000 --- a/gio/gpu/internal/opengl/srgb.go +++ /dev/null @@ -1,176 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package opengl - -import ( - "errors" - "fmt" - "image" - "runtime" - "strings" - - "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" - "github.com/p9c/p9/pkg/gel/gio/internal/gl" -) - -// SRGBFBO implements an intermediate sRGB FBO -// for gamma-correct rendering on platforms without -// sRGB enabled native framebuffers. -type SRGBFBO struct { - c *gl.Functions - state *glState - viewport image.Point - fbo gl.Framebuffer - tex gl.Texture - blitted bool - quad gl.Buffer - prog gl.Program - format textureTriple -} - -func NewSRGBFBO(f *gl.Functions, state *glState) (*SRGBFBO, error) { - glVer := f.GetString(gl.VERSION) - ver, _, err := gl.ParseGLVersion(glVer) - if err != nil { - return nil, err - } - exts := strings.Split(f.GetString(gl.EXTENSIONS), " ") - srgbTriple, err := srgbaTripleFor(ver, exts) - if err != nil { - // Fall back to the linear RGB colorspace, at the cost of color precision loss. - srgbTriple = textureTriple{gl.RGBA, gl.Enum(gl.RGBA), gl.Enum(gl.UNSIGNED_BYTE)} - } - s := &SRGBFBO{ - c: f, - state: state, - format: srgbTriple, - fbo: f.CreateFramebuffer(), - tex: f.CreateTexture(), - } - state.bindTexture(f, 0, s.tex) - f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) - f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) - f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST) - f.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST) - return s, nil -} - -func (s *SRGBFBO) Blit() { - if !s.blitted { - prog, err := gl.CreateProgram(s.c, blitVSrc, blitFSrc, []string{"pos", "uv"}) - if err != nil { - panic(err) - } - s.prog = prog - s.state.useProgram(s.c, prog) - s.c.Uniform1i(s.c.GetUniformLocation(prog, "tex"), 0) - s.quad = s.c.CreateBuffer() - s.state.bindBuffer(s.c, gl.ARRAY_BUFFER, s.quad) - coords := byteslice.Slice([]float32{ - -1, +1, 0, 1, - +1, +1, 1, 1, - -1, -1, 0, 0, - +1, -1, 1, 0, - }) - s.c.BufferData(gl.ARRAY_BUFFER, len(coords), gl.STATIC_DRAW, coords) - s.blitted = true - } - s.state.useProgram(s.c, s.prog) - s.state.bindTexture(s.c, 0, s.tex) - s.state.vertexAttribPointer(s.c, s.quad, 0 /* pos */, 2, gl.FLOAT, false, 4*4, 0) - s.state.vertexAttribPointer(s.c, s.quad, 1 /* uv */, 2, gl.FLOAT, false, 4*4, 4*2) - s.state.setVertexAttribArray(s.c, 0, true) - s.state.setVertexAttribArray(s.c, 1, true) - s.c.DrawArrays(gl.TRIANGLE_STRIP, 0, 4) - s.state.bindFramebuffer(s.c, gl.FRAMEBUFFER, s.fbo) - s.c.InvalidateFramebuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0) -} - -func (s *SRGBFBO) Framebuffer() gl.Framebuffer { - return s.fbo -} - -func (s *SRGBFBO) Refresh(viewport image.Point) error { - if viewport.X == 0 || viewport.Y == 0 { - return errors.New("srgb: zero-sized framebuffer") - } - if s.viewport == viewport { - return nil - } - s.viewport = viewport - s.state.bindTexture(s.c, 0, s.tex) - s.c.TexImage2D(gl.TEXTURE_2D, 0, s.format.internalFormat, viewport.X, viewport.Y, s.format.format, s.format.typ) - s.state.bindFramebuffer(s.c, gl.FRAMEBUFFER, s.fbo) - s.c.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, s.tex, 0) - if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { - return fmt.Errorf("sRGB framebuffer incomplete (%dx%d), status: %#x error: %x", viewport.X, viewport.Y, st, s.c.GetError()) - } - - if runtime.GOOS == "js" { - // With macOS Safari, rendering to and then reading from a SRGB8_ALPHA8 - // texture result in twice gamma corrected colors. Using a plain RGBA - // texture seems to work. - s.state.setClearColor(s.c, .5, .5, .5, 1.0) - s.c.Clear(gl.COLOR_BUFFER_BIT) - var pixel [4]byte - s.c.ReadPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel[:]) - if pixel[0] == 128 { // Correct sRGB color value is ~188 - s.c.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, viewport.X, viewport.Y, gl.RGBA, gl.UNSIGNED_BYTE) - if st := s.c.CheckFramebufferStatus(gl.FRAMEBUFFER); st != gl.FRAMEBUFFER_COMPLETE { - return fmt.Errorf("fallback RGBA framebuffer incomplete (%dx%d), status: %#x error: %x", viewport.X, viewport.Y, st, s.c.GetError()) - } - } - } - - return nil -} - -func (s *SRGBFBO) Release() { - s.state.deleteFramebuffer(s.c, s.fbo) - s.state.deleteTexture(s.c, s.tex) - if s.blitted { - s.state.deleteBuffer(s.c, s.quad) - s.state.deleteProgram(s.c, s.prog) - } - s.c = nil -} - -const ( - blitVSrc = ` -#version 100 - -precision highp float; - -attribute vec2 pos; -attribute vec2 uv; - -varying vec2 vUV; - -void main() { - gl_Position = vec4(pos, 0, 1); - vUV = uv; -} -` - blitFSrc = ` -#version 100 - -precision mediump float; - -uniform sampler2D tex; -varying vec2 vUV; - -vec3 gamma(vec3 rgb) { - vec3 exp = vec3(1.055)*pow(rgb, vec3(0.41666)) - vec3(0.055); - vec3 lin = rgb * vec3(12.92); - bvec3 cut = lessThan(rgb, vec3(0.0031308)); - return vec3(cut.r ? lin.r : exp.r, cut.g ? lin.g : exp.g, cut.b ? lin.b : exp.b); -} - -void main() { - vec4 col = texture2D(tex, vUV); - vec3 rgb = col.rgb; - rgb = gamma(rgb); - gl_FragColor = vec4(rgb, col.a); -} -` -) diff --git a/gio/gpu/internal/rendertest/bench_test.go b/gio/gpu/internal/rendertest/bench_test.go deleted file mode 100644 index 5375a1a..0000000 --- a/gio/gpu/internal/rendertest/bench_test.go +++ /dev/null @@ -1,309 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package rendertest - -import ( - "image" - "image/color" - "math" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/font/gofont" - "github.com/p9c/p9/pkg/gel/gio/gpu/headless" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/widget/material" -) - -// use some global variables for benchmarking so as to not pollute -// the reported allocs with allocations that we do not want to count. -var ( - c1, c2, c3 = make(chan op.CallOp), make(chan op.CallOp), make(chan op.CallOp) - op1, op2, op3 op.Ops -) - -func setupBenchmark(b *testing.B) (layout.Context, *headless.Window, *material.Theme) { - sz := image.Point{X: 1024, Y: 1200} - w := newWindow(b, sz.X, sz.Y) - ops := new(op.Ops) - gtx := layout.Context{ - Ops: ops, - Constraints: layout.Exact(sz), - } - th := material.NewTheme() - th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection())) - return gtx, w, th -} - -func resetOps(gtx layout.Context) { - gtx.Ops.Reset() - op1.Reset() - op2.Reset() - op3.Reset() -} - -func finishBenchmark(b *testing.B, w *headless.Window) { - b.StopTimer() - if *dumpImages { - img := image.NewRGBA(image.Rectangle{Max: w.Size()}) - err := w.Screenshot(img) - w.Release() - if err != nil { - b.Error(err) - } - saveImage(b, b.Name()+".png", img) - } -} - -func BenchmarkDrawUICached(b *testing.B) { - // As BenchmarkDraw but the same op.Ops every time that is not reset - this - // should thus allow for maximal cache usage. - gtx, w, th := setupBenchmark(b) - defer w.Release() - drawCore(gtx, th) - w.Frame(gtx.Ops) - - for b.Loop() { - w.Frame(gtx.Ops) - } - finishBenchmark(b, w) -} - -func BenchmarkDrawUI(b *testing.B) { - // BenchmarkDraw is intended as a reasonable overall benchmark for - // the drawing performance of the full drawing pipeline, in each iteration - // resetting the ops and drawing, similar to how a typical UI would function. - // This will allow font caching across frames. - gtx, w, th := setupBenchmark(b) - defer w.Release() - drawCore(gtx, th) - w.Frame(gtx.Ops) - b.ReportAllocs() - - for i := 0; b.Loop(); i++ { - resetOps(gtx) - - off := float32(math.Mod(float64(i)/10, 10)) - t := op.Affine(f32.AffineId().Offset(f32.Pt(off, off))).Push(gtx.Ops) - - drawCore(gtx, th) - - t.Pop() - w.Frame(gtx.Ops) - } - finishBenchmark(b, w) -} - -func BenchmarkDrawUITransformed(b *testing.B) { - // Like BenchmarkDraw UI but transformed at every frame - gtx, w, th := setupBenchmark(b) - defer w.Release() - drawCore(gtx, th) - w.Frame(gtx.Ops) - b.ReportAllocs() - - for i := 0; b.Loop(); i++ { - resetOps(gtx) - - angle := float32(math.Mod(float64(i)/1000, 0.05)) - a := f32.AffineId().Shear(f32.Point{}, angle, angle).Rotate(f32.Point{}, angle) - t := op.Affine(a).Push(gtx.Ops) - - drawCore(gtx, th) - - t.Pop() - w.Frame(gtx.Ops) - } - finishBenchmark(b, w) -} - -func Benchmark1000Circles(b *testing.B) { - // Benchmark1000Shapes draws 1000 individual shapes such that no caching between - // shapes will be possible and resets buffers on each operation to prevent caching - // between frames. - gtx, w, _ := setupBenchmark(b) - defer w.Release() - draw1000Circles(gtx) - w.Frame(gtx.Ops) - b.ReportAllocs() - - for b.Loop() { - resetOps(gtx) - draw1000Circles(gtx) - w.Frame(gtx.Ops) - } - finishBenchmark(b, w) -} - -func Benchmark1000CirclesInstanced(b *testing.B) { - // Like Benchmark1000Circles but will record them and thus allow for caching between - // them. - gtx, w, _ := setupBenchmark(b) - defer w.Release() - draw1000CirclesInstanced(gtx) - w.Frame(gtx.Ops) - b.ReportAllocs() - - for b.Loop() { - resetOps(gtx) - draw1000CirclesInstanced(gtx) - w.Frame(gtx.Ops) - } - finishBenchmark(b, w) -} - -func draw1000Circles(gtx layout.Context) { - ops := gtx.Ops - for x := range 100 { - op.Offset(image.Pt(x*10, 0)).Add(ops) - for y := range 10 { - paint.FillShape(ops, - color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}, - clip.RRect{Rect: image.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Op(ops), - ) - op.Offset(image.Pt(0, 100)).Add(ops) - } - } -} - -func draw1000CirclesInstanced(gtx layout.Context) { - ops := gtx.Ops - - r := op.Record(ops) - cl := clip.RRect{Rect: image.Rect(0, 0, 10, 10), NE: 5, SE: 5, SW: 5, NW: 5}.Push(ops) - paint.PaintOp{}.Add(ops) - cl.Pop() - c := r.Stop() - - for x := range 100 { - op.Offset(image.Pt(x*10, 0)).Add(ops) - for y := range 10 { - paint.ColorOp{Color: color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}}.Add(ops) - c.Add(ops) - op.Offset(image.Pt(0, 100)).Add(ops) - } - } -} - -func drawCore(gtx layout.Context, th *material.Theme) { - c1 := drawIndividualShapes(gtx, th) - c2 := drawShapeInstances(gtx, th) - c3 := drawText(gtx, th) - - (<-c1).Add(gtx.Ops) - (<-c2).Add(gtx.Ops) - (<-c3).Add(gtx.Ops) -} - -func drawIndividualShapes(gtx layout.Context, th *material.Theme) chan op.CallOp { - // draw 81 rounded rectangles of different solid colors - each one individually - go func() { - ops := &op1 - c := op.Record(ops) - for x := range 9 { - op.Offset(image.Pt(x*50, 0)).Add(ops) - for y := range 9 { - paint.FillShape(ops, - color.NRGBA{R: 100 + uint8(x), G: 100 + uint8(y), B: 100, A: 120}, - clip.RRect{Rect: image.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Op(ops), - ) - op.Offset(image.Pt(0, 50)).Add(ops) - } - } - c1 <- c.Stop() - }() - return c1 -} - -func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp { - // draw 400 textured circle instances, each with individual transform - go func() { - ops := &op2 - co := op.Record(ops) - - r := op.Record(ops) - cl := clip.RRect{Rect: image.Rect(0, 0, 25, 25), NE: 10, SE: 10, SW: 10, NW: 10}.Push(ops) - paint.PaintOp{}.Add(ops) - cl.Pop() - c := r.Stop() - - squares.Add(ops) - rad := float32(0) - for x := range 20 { - for y := range 20 { - t := op.Offset(image.Pt(x*50+25, y*50+25)).Push(ops) - c.Add(ops) - t.Pop() - rad += math.Pi * 2 / 400 - } - } - c2 <- co.Stop() - }() - return c2 -} - -func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp { - // draw 40 lines of text with different transforms. - go func() { - ops := &op3 - c := op.Record(ops) - - txt := material.H6(th, "") - for x := range 40 { - txt.Text = textRows[x] - t := op.Offset(image.Pt(0, 24*x)).Push(ops) - gtx.Ops = ops - txt.Layout(gtx) - t.Pop() - } - c3 <- c.Stop() - }() - return c3 -} - -var textRows = []string{ - "1. I learned from my grandfather, Verus, to use good manners, and to", - "put restraint on anger. 2. In the famous memory of my father I had a", - "pattern of modesty and manliness. 3. Of my mother I learned to be", - "pious and generous; to keep myself not only from evil deeds, but even", - "from evil thoughts; and to live with a simplicity which is far from", - "customary among the rich. 4. I owe it to my great-grandfather that I", - "did not attend public lectures and discussions, but had good and able", - "teachers at home; and I owe him also the knowledge that for things of", - "this nature a man should count no expense too great.", - "5. My tutor taught me not to favour either green or blue at the", - "chariot races, nor, in the contests of gladiators, to be a supporter", - "either of light or heavy armed. He taught me also to endure labour;", - "not to need many things; to serve myself without troubling others; not", - "to intermeddle in the affairs of others, and not easily to listen to", - "slanders against them.", - "6. Of Diognetus I had the lesson not to busy myself about vain things;", - "not to credit the great professions of such as pretend to work", - "wonders, or of sorcerers about their charms, and their expelling of", - "Demons and the like; not to keep quails (for fighting or divination),", - "nor to run after such things; to suffer freedom of speech in others,", - "and to apply myself heartily to philosophy. Him also I must thank for", - "my hearing first Bacchius, then Tandasis and Marcianus; that I wrote", - "dialogues in my youth, and took a liking to the philosopher's pallet", - "and skins, and to the other things which, by the Grecian discipline,", - "belong to that profession.", - "7. To Rusticus I owe my first apprehensions that my nature needed", - "reform and cure; and that I did not fall into the ambition of the", - "common Sophists, either by composing speculative writings or by", - "declaiming harangues of exhortation in public; further, that I never", - "strove to be admired by ostentation of great patience in an ascetic", - "life, or by display of activity and application; that I gave over the", - "study of rhetoric, poetry, and the graces of language; and that I did", - "not pace my house in my senatorial robes, or practise any similar", - "affectation. I observed also the simplicity of style in his letters,", - "particularly in that which he wrote to my mother from Sinuessa. I", - "learned from him to be easily appeased, and to be readily reconciled", - "with those who had displeased me or given cause of offence, so soon as", - "they inclined to make their peace; to read with care; not to rest", - "satisfied with a slight and superficial knowledge; nor quickly to", - "assent to great talkers. I have him to thank that I met with the", -} diff --git a/gio/gpu/internal/rendertest/clip_test.go b/gio/gpu/internal/rendertest/clip_test.go deleted file mode 100644 index 3b8f645..0000000 --- a/gio/gpu/internal/rendertest/clip_test.go +++ /dev/null @@ -1,326 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package rendertest - -import ( - "image" - "image/color" - "math" - "testing" - - "golang.org/x/image/colornames" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -func TestPaintRect(t *testing.T) { - run(t, func(o *op.Ops) { - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) - }, func(r result) { - r.expect(0, 0, colornames.Red) - r.expect(49, 0, colornames.Red) - r.expect(50, 0, transparent) - r.expect(10, 50, transparent) - }) -} - -func TestPaintClippedRect(t *testing.T) { - run(t, func(o *op.Ops) { - defer clip.RRect{Rect: image.Rect(25, 25, 60, 60)}.Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(24, 35, transparent) - r.expect(25, 35, colornames.Red) - r.expect(50, 0, transparent) - r.expect(10, 50, transparent) - }) -} - -func TestPaintClippedRectOffset(t *testing.T) { - run(t, func(o *op.Ops) { - defer op.Affine(f32.AffineId().Offset(f32.Pt(0.5, 0.5))).Push(o).Pop() - defer clip.RRect{Rect: image.Rect(25, 25, 60, 60)}.Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(24, 35, transparent) - r.expect(24, 24, transparent) - r.expect(25, 25, color.RGBA{R: 137, A: 64}) - r.expect(25, 35, color.RGBA{R: 187, A: 128}) - r.expect(35, 25, color.RGBA{R: 187, A: 128}) - r.expect(50, 50, color.RGBA{R: 137, A: 64}) - r.expect(51, 51, transparent) - r.expect(50, 0, transparent) - r.expect(10, 50, transparent) - }) -} - -func TestPaintClippedCircle(t *testing.T) { - run(t, func(o *op.Ops) { - const r = 10 - defer clip.RRect{Rect: image.Rect(20, 20, 40, 40), SE: r, SW: r, NW: r, NE: r}.Push(o).Pop() - defer clip.Rect(image.Rect(0, 0, 30, 50)).Push(o).Pop() - paint.Fill(o, red) - }, func(r result) { - r.expect(21, 21, transparent) - r.expect(25, 30, colornames.Red) - r.expect(31, 30, transparent) - }) -} - -func TestPaintArc(t *testing.T) { - run(t, func(o *op.Ops) { - p := new(clip.Path) - p.Begin(o) - p.Move(f32.Pt(0, 20)) - p.Line(f32.Pt(10, 0)) - p.Arc(f32.Pt(10, 0), f32.Pt(40, 0), math.Pi) - p.Line(f32.Pt(30, 0)) - p.Line(f32.Pt(0, 25)) - p.Arc(f32.Pt(-10, 5), f32.Pt(10, 15), -math.Pi) - p.Line(f32.Pt(0, 25)) - p.Arc(f32.Pt(10, 10), f32.Pt(10, 10), 2*math.Pi) - p.Line(f32.Pt(-10, 0)) - p.Arc(f32.Pt(-10, 0), f32.Pt(-40, 0), -math.Pi) - p.Line(f32.Pt(-10, 0)) - p.Line(f32.Pt(0, -10)) - p.Arc(f32.Pt(-10, -20), f32.Pt(10, -5), math.Pi) - p.Line(f32.Pt(0, -10)) - p.Line(f32.Pt(-50, 0)) - p.Close() - defer clip.Outline{ - Path: p.End(), - }.Op().Push(o).Pop() - - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(0, 25, colornames.Red) - r.expect(0, 15, transparent) - }) -} - -func TestPaintAbsolute(t *testing.T) { - run(t, func(o *op.Ops) { - p := new(clip.Path) - p.Begin(o) - p.Move(f32.Pt(100, 100)) // offset the initial pen position to test "MoveTo" - - p.MoveTo(f32.Pt(20, 20)) - p.LineTo(f32.Pt(80, 20)) - p.QuadTo(f32.Pt(80, 80), f32.Pt(20, 80)) - p.Close() - defer clip.Outline{ - Path: p.End(), - }.Op().Push(o).Pop() - - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 128, 128)).Op()) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(30, 30, colornames.Red) - r.expect(79, 79, transparent) - r.expect(90, 90, transparent) - }) -} - -func TestPaintTexture(t *testing.T) { - run(t, func(o *op.Ops) { - squares.Add(o) - defer scale(80.0/512, 80.0/512).Push(o).Pop() - paint.PaintOp{}.Add(o) - }, func(r result) { - r.expect(0, 0, colornames.Blue) - r.expect(79, 10, colornames.Green) - r.expect(80, 0, transparent) - r.expect(10, 80, transparent) - }) -} - -func TestTexturedStrokeClipped(t *testing.T) { - run(t, func(o *op.Ops) { - smallSquares.Add(o) - defer op.Offset(image.Pt(50, 50)).Push(o).Pop() - defer clip.Stroke{ - Path: clip.RRect{Rect: image.Rect(0, 0, 30, 30)}.Path(o), - Width: 10, - }.Op().Push(o).Pop() - defer clip.RRect{Rect: image.Rect(-30, -30, 60, 60)}.Push(o).Pop() - defer op.Offset(image.Pt(-10, -10)).Push(o).Pop() - paint.PaintOp{}.Add(o) - }, nil) -} - -func TestTexturedStroke(t *testing.T) { - run(t, func(o *op.Ops) { - smallSquares.Add(o) - defer op.Offset(image.Pt(50, 50)).Push(o).Pop() - defer clip.Stroke{ - Path: clip.RRect{Rect: image.Rect(0, 0, 30, 30)}.Path(o), - Width: 10, - }.Op().Push(o).Pop() - defer op.Offset(image.Pt(-10, -10)).Push(o).Pop() - paint.PaintOp{}.Add(o) - }, nil) -} - -func TestPaintClippedTexture(t *testing.T) { - run(t, func(o *op.Ops) { - squares.Add(o) - defer clip.RRect{Rect: image.Rect(0, 0, 40, 40)}.Push(o).Pop() - defer scale(80.0/512, 80.0/512).Push(o).Pop() - paint.PaintOp{}.Add(o) - }, func(r result) { - r.expect(40, 40, transparent) - r.expect(25, 35, colornames.Blue) - }) -} - -func TestStrokedPathZeroWidth(t *testing.T) { - t.Skip("test broken, see issue 479") - - run(t, func(o *op.Ops) { - { - p := new(clip.Path) - p.Begin(o) - p.Move(f32.Pt(10, 50)) - p.Line(f32.Pt(30, 0)) - cl := clip.Stroke{ - Path: p.End(), - }.Op().Push(o) // width=0, disable stroke - - paint.Fill(o, red) - cl.Pop() - } - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(10, 50, colornames.Black) - r.expect(30, 50, colornames.Black) - r.expect(65, 50, transparent) - }) -} - -func TestStrokedPathCoincidentControlPoint(t *testing.T) { - run(t, func(o *op.Ops) { - p := new(clip.Path) - p.Begin(o) - p.MoveTo(f32.Pt(70, 20)) - p.CubeTo(f32.Pt(70, 20), f32.Pt(70, 110), f32.Pt(120, 120)) - p.LineTo(f32.Pt(20, 120)) - p.LineTo(f32.Pt(70, 20)) - cl := clip.Stroke{ - Path: p.End(), - Width: 20, - }.Op().Push(o) - - paint.Fill(o, black) - cl.Pop() - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(70, 20, colornames.Black) - r.expect(70, 90, transparent) - }) -} - -func TestStrokedPathBalloon(t *testing.T) { - run(t, func(o *op.Ops) { - // This shape is based on the one drawn by the Bubble function in - // github.com/llgcode/draw2d/samples/geometry/geometry.go. - p := new(clip.Path) - p.Begin(o) - p.MoveTo(f32.Pt(42.69375, 10.5)) - p.CubeTo(f32.Pt(42.69375, 10.5), f32.Pt(14.85, 10.5), f32.Pt(14.85, 31.5)) - p.CubeTo(f32.Pt(14.85, 31.5), f32.Pt(14.85, 52.5), f32.Pt(28.771875, 52.5)) - p.CubeTo(f32.Pt(28.771875, 52.5), f32.Pt(28.771875, 63.7), f32.Pt(17.634375, 66.5)) - p.CubeTo(f32.Pt(17.634375, 66.5), f32.Pt(34.340626, 63.7), f32.Pt(37.125, 52.5)) - p.CubeTo(f32.Pt(37.125, 52.5), f32.Pt(70.5375, 52.5), f32.Pt(70.5375, 31.5)) - p.CubeTo(f32.Pt(70.5375, 31.5), f32.Pt(70.5375, 10.5), f32.Pt(42.69375, 10.5)) - cl := clip.Stroke{ - Path: p.End(), - Width: 2.83, - }.Op().Push(o) - paint.Fill(o, black) - cl.Pop() - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(70, 52, colornames.Black) - r.expect(70, 90, transparent) - }) -} - -func TestPathReuse(t *testing.T) { - run(t, func(o *op.Ops) { - var path clip.Path - path.Begin(o) - path.MoveTo(f32.Pt(60, 10)) - path.LineTo(f32.Pt(110, 75)) - path.LineTo(f32.Pt(10, 75)) - path.Close() - spec := path.End() - - outline := clip.Outline{Path: spec}.Op().Push(o) - paint.Fill(o, color.NRGBA{R: 0xFF, A: 0xFF}) - outline.Pop() - - stroke := clip.Stroke{Path: spec, Width: 3}.Op().Push(o) - paint.Fill(o, color.NRGBA{B: 0xFF, A: 0xFF}) - stroke.Pop() - }, nil) -} - -func TestPathInterleave(t *testing.T) { - t.Run("interleave op in clip.Path", func(t *testing.T) { - defer func() { - if err := recover(); err == nil { - t.Error("expected panic did not occur") - } - }() - ops := new(op.Ops) - var path clip.Path - path.Begin(ops) - path.LineTo(f32.Point{X: 123, Y: 456}) - paint.ColorOp{}.Add(ops) - path.End() - }) - t.Run("use ops after clip.Path", func(t *testing.T) { - ops := new(op.Ops) - var path clip.Path - path.Begin(ops) - path.LineTo(f32.Point{X: 123, Y: 456}) - path.End() - paint.ColorOp{}.Add(ops) - }) -} - -func TestStrokedRect(t *testing.T) { - run(t, func(o *op.Ops) { - r := clip.Rect{Min: image.Pt(50, 50), Max: image.Pt(100, 100)} - paint.FillShape(o, - color.NRGBA{R: 0xff, A: 0xFF}, - clip.Stroke{ - Path: r.Path(), - Width: 5, - }.Op(), - ) - }, nil) -} - -func TestInstancedRects(t *testing.T) { - run(t, func(o *op.Ops) { - macro := op.Record(o) - clip := clip.Rect{Max: image.Pt(20, 20)}.Push(o) - paint.ColorOp{Color: color.NRGBA{R: 0x80, A: 0xFF}}.Add(o) - paint.PaintOp{}.Add(o) - clip.Pop() - c := macro.Stop() - - for range 2 { - op.Affine(f32.AffineId().Rotate(f32.Pt(0, 0), .2)).Add(o) - c.Add(o) - op.Offset(image.Pt(20, 20)).Add(o) - } - }, nil) -} diff --git a/gio/gpu/internal/rendertest/doc.go b/gio/gpu/internal/rendertest/doc.go deleted file mode 100644 index 378f1fb..0000000 --- a/gio/gpu/internal/rendertest/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package rendertest is intended for testing of drawing ops only. -package rendertest diff --git a/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen.png deleted file mode 100644 index fb50427e4abfbeab0128c74e31584ab47426ed58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2_SGb59q?kczms2Mrm4yh97V`seb< fF90&}frjmj4Gj#8E6=|Z1u64%^>bP0l+XkKZXy*1 diff --git a/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png b/gio/gpu/internal/rendertest/refs/TestBuildOffscreen_1.png deleted file mode 100644 index 8ff717bad956c1fcd6e2a9e6faa830e9f0d355b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 413 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2{2-Fs69AIEGZ*dV6bk?;!_~)`!Z9 zjaxjHwDq{1$YAy=WWK;KEtW-al4gsT!oedpC7ctSyJM@H_CEf_WEx8o8QcOytsyMe~n(3KH>9db{=WASEY*=H>pL@qK=Bz zt$1|(;cHEk9@T#l$L)`s&+L;rE>OQ@Q)tcPl~+#GGgiL-p%(nE{^jPr$5obdR=tXG zSi15))24GDeV3hizwW){l7{8i?>;p1sF|Gk+VG#t^aR!zlaJvCj3R&nFV{DiWH7GW z&&21*ZSg~U0auj6)AbD8HX;s;A_tUCq)+G-h)%3)`yu{WU}<0wU|>SyFc1g!-@b95 UNuzF-&L@z1Pgg&ebxsLQ03$ghasU7T diff --git a/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png b/gio/gpu/internal/rendertest/refs/TestClipPaintOffset.png deleted file mode 100644 index 0fe37e6fa6f11d23f7c2d9e553a7f2b47cf25fbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSC7v#hAr-gYUUKAOP~c#2eEYxN z?*prBi$>aqH44&pfi@Ln>~)y}psR!9jr8k-7IW z*V{lAcPC~R507~h_ccbV&j%_)0W}$uj*5E4p0qnzs5U#h~#P!8IFj@*NBVAe-xDe_;aV{G*7;d-E&WVD`I&h-tX||kzhx>4b#VNcZIr-97pv3 jDXn|NjA8|L&IfMuWi$3L%b3i41XAGX>gTe~DWM4fue3uF diff --git a/gio/gpu/internal/rendertest/refs/TestClipScale.png b/gio/gpu/internal/rendertest/refs/TestClipScale.png deleted file mode 100644 index 6396fb4f2ca971e734f2f4bc6328cc622669d8d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSb)GJcAr-gYUTWlIP~dPqnE(5I zjxkH4NI7@MFCm~j5_rICYPw_czC9k5&v!m$VRB&LU|>{WU}<0wU|>SyFc1g!-@b95 UNuzF-&L@z1Pgg&ebxsLQ03$ghasU7T diff --git a/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png b/gio/gpu/internal/rendertest/refs/TestComplicatedTransform.png deleted file mode 100644 index d1adb55d4557bec88de2736bfcf689621a1aed82..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 569 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU}EreaSW-r_4fA0ERjHowuk=K z*0V*DG8abNkknz>-WhJlXuX?bHD}Uh_3#BZZg5O96Z^KgcWDJH*G9qBuDT*Y9z{A# zi`XXJHD#Xpu<}9r>BgV;nNRafo>QhO9#mvK`Oaw-|KLwns`doA z_J6j1sdr~#HaxhOCRb~vyo2{dvbGcO27KMpiI zUi$84MbPGL#T%AXF3-$`_t8Q1OEz49|VZv7-z z?B;<)7nonW9r#?n?Z=NNQ$A}+3IgMnXb}60_4hGLo6;)20+1e0S3j3^P6yH zY+Yd*(9o&bcIkvh6PL%n8F>p@1?$arJwKwI-MpI9ZCY9U``3jZD{og>{pO1Q`2G!d zfga-#P6a=PLZ%L3hdB(7SOnA?cGT8<`uXj{V)LJy`yTFmJ4xDE7)^x*NK@WEPMNBm z^?QH6%a6PH|Mu+{cMM}B^7jAfU&skG1gr$r+yN1KoBb#3?VFFcUq9l3N+3%tQ$uBq=Pau?gLA2gcd||Lbn@ZYr;hj|OrWJYD@<);T3K F0RYUjr^)~T diff --git a/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png b/gio/gpu/internal/rendertest/refs/TestDepthOverlap.png deleted file mode 100644 index 9d416b9fa6767e124baa30c79662b80008d35af6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 303 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRS-#lF$Ln>~)y>d{HL4kw$V8YXq zb>9})m$6&z@Lamt_{O`QZA}aU3``CT91M&K3@i<_;xv?(=6{N2;HH@c2)8%r^Ic45 VscSL5`-=exJYD@<);T3K0RX!VGJF64 diff --git a/gio/gpu/internal/rendertest/refs/TestGapsInPath/Outline.png b/gio/gpu/internal/rendertest/refs/TestGapsInPath/Outline.png deleted file mode 100644 index aa0a7749307cffe20070cb4ef5d1e2a78fe15571..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 408 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV2t;4aSW-L^XAGy&ISVk)>S%XoxS!H=PksYBRd4#OiB0rdt8 r(u5j}|5NGgo^>bP0l+XkKfva(K diff --git a/gio/gpu/internal/rendertest/refs/TestGapsInPath/Stroke.png b/gio/gpu/internal/rendertest/refs/TestGapsInPath/Stroke.png deleted file mode 100644 index 1420f6b7ae73562e325f458e595e1a3501ab1c3b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 488 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU|jF%;uumf=gnySyKp|cv9ztk{ScqX9 zr^}{UHrNYKZkpU>6erZ}KK>UvfL+7Y(7(o6EBTQeQLMO69Yt=Fqd#dw<1$^51|d~D zs+%8Fk=b_C=>0nSxjJHzV36%oXYxeQ#}2DYzVrcQC*Thnzc%Z+VF(Us{qKSBw+9z> zy5th&|2mK5loD-}rsO8i|7#YN&uGxh(sZ7AlFsqIa!oD2sc5xEeIxMsFiugZHJ-ML zAqtIuN*$hRyGuR-Iu-5j?Z0M;oAdQ1hfWZ^;L<+l(~lQ8hG=W_(<8Np#!kkQ!iblQ zQb1(iQ)TFLnkwlu(A^B>MLQFtVlM%DfN28)@I8UAZtgp5+eHi5_0Eh4CQ{stL2QO6}^jMb-8`LYdQc++Y)3taWaG8Oqr@pSU zOeU?4Phlt3zf{?YxR;E-Wx>dXixdL`V-e{!*~TkSi0i+fT8@F)Mav_ADVmzy`uP~u zbltg`k<*GT*UXej@Bxp>)&L|o1COm>l=pU(Jnehn>CP)uP402i#ro@TD_<%u)^g)L z92IuUuZM&W59@?tz&N(oZS>5ba{tyTkq(h}35UW9QFSick!gLLKMSab75TdQzA@n0 z8-FLkHr%Yb%&93ymugY?S=ArX!@{FGd{Mb?xR1tz0NXeT_O5rDmlE5Ec{(B?a?ee- zwx&Y*@f`~nQmXiEyc5^ZNK@WNUbaQ+uAVH5rG_}2zYiCeeV8Q1s^jX~{oFw-nh9ZY zdBO`ybWO=BDT!~Si+9>15T*HGL#6)A(%H=Hg{yOGspS_gR{z=rfjrsOyyn_I3g`le zgdqHZ(9NgWN8U;>F&Kc~%hqbSY2!Q-(f-Ibs-qFvV|KEa*wy2mRqvpGe0SYkG0O6E zLl-BK>cJs*IRlL$?m6pWm9$k?Y{dZGq&`88w@0(H~V3B9ntR_<@fOL6|%Zhsxb< zTGB&tQ?{r%b+3B{DFSm(mX&ZAgb%glWZ5S~oIJ=se9N|qn*>XB&@Z}qQH>BTw7k9E zDaC54@Q=FwEEv4#qsVn?JHF+<-hM0hY>?xTf#BrFge(CYdVMs)lr?{fq?fx+cZ3@b z#wt5x<28qx=#j_KHFF2*(FL6PtO6;%S1QGD~^a>+t$Zt-T7y>!S=lw>7tWpBiydlTC7DY zH@%u12yMk@9FlanS63cHeLF+I3r>lrt78jqrCQ`I)4tm%D%tmP=6G7>d9Z{K@t!3^C&NZY1U0SxQvZ zKi+P(3gO{DqX2$s6yqf=a6)?01&_CwA0WO|xNMR7)=ERb_`vUmGoWkt5aM?Vq93N9 z>Qf+Td+;wxBtO>X>%PqF<@*JjnK;09u@>Ft$`b|@Uz24vC6X6c|iWYN2fTtkxq6j zPqzQ8w*<%;*9Fag5xLhqHq~G+9Or+%Fe9&QgIwUsYJwebKoEY zUn)a)NC3os-%_nyk}!jjwsm0(0)r=-&_-rZI?2wsm6d4LLY&2RXC~IAHssfjr?Y{Yv8FA9z zeZuA`Z@J7nqY}w|{=GoPldTIHxoqpL8ueOYh%+Ag7p9J*C7v*y3(jk-nQ*$*9G6+( z`^=15o_3n~j;+%+9T07mMQq zHU0peVbU4*y9D1{mcPaTjvYWV@4ZA2sMml%u{U^fTSu?o5)>3rfX20)}7py&$QE9N7u0GT!+N@?MlYm z62Mp=E0Q*6fv2d~S_D=LK3c?;>jNgFMEDZ_us$>j9CluPek$0PQi#pPTZS6Ig2DY; zM;HsZ&Tg-%_@Gq4#c?Rso- z7_3BUQ$4Lm^628m_s4>}ljl?{0X{V22w}I1Ka*;mO6bG~$V50$h-)4lK9HR8=Ynj3 zFHn+tRrXRLjsp`CB{tKzea)g4 zY0i8dh)(FV{h=2s3TlJE&M!>@yKE7lM(|wZgZN=G!F^v!|7k4pWiDP+UQx?dP&a0} zzW)Z!-*0xp`NZHvg60?;ZQQp!SPP8#-FzMtQ3#IE zRFlfTO_ac)xZ3dYW8iiTnMYh*#U2H9z38Rqo}5X#3sAD;Rm$!e@_XiWVL%~$>RjBa zmn?~!)G+j8?=1eiml&)-48d>hPJRHe?hHe>`hweLJ;46_J!5#e3z*%VdxkKLwMnT~ z`?PY_tE9^skoeWaw8DV%Q-5g?1iDdJX->MblbF^u88ucM>ee%FvWmaD^|yb2jLwx4 zNcS2oX?Kt{cZYl>WQKvC58^#}xK1~3`H?&kzjR9hP{sO7{!EV8hNU@yW;j(5N%N2g zHUw0ttUMWjQpu{3ntE%4#h#Rm7r8#uWm~E>6VWM_q_$@ywtPZJqYCKk0@AnnzlU!E c00@Oh;_R~sz4`249~@v~VQ>EOf^X{o0MD?rZU6uP diff --git a/gio/gpu/internal/rendertest/refs/TestImageRGBA_ScaleNearest.png b/gio/gpu/internal/rendertest/refs/TestImageRGBA_ScaleNearest.png deleted file mode 100644 index ae3ce430313fcd2f2e2938206f4717076ff9c55e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 379 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1SEZ8zRh7^V07|yaSW-L^XBeB&I1Yptbx2@ z^UnDS%E?Wt6s?i?wkNxP_t|iLi_b3@>%$q=FhnqJU`k*;z||nipv@3Qe}T0d_5Ujv oERQQ#!aZ<~K=Qjj1H=FSzin7rjf8G41O_UDr>mdKI;Vst0Hcg#RR910 diff --git a/gio/gpu/internal/rendertest/refs/TestInstancedRects.png b/gio/gpu/internal/rendertest/refs/TestInstancedRects.png deleted file mode 100644 index e5f195a8b36aa5eee87ed70e5495cdf70533bbd3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 787 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV7le$;uunK>+S9P+5Uwx$3H%w zBL6egOfhxqr^l0Gc1-AQUDL+xwWHu^+w8j+!fu@V{ic1&qS(jBSh?3~Y$|fqi_sAe z^3pJMH8GzYe>)~;>6czp>#d*v+Q^swYrOvZtL5|0md}5yCTKh;b8vSkKHxQ_ui+!t z_1g{qWc*Suhy%m z>wfHT!+Ob>vkBduDocdT!fpuLa9T_{TqC^d-_`n;`m+yzXS9`EW4Q8_OkM7Q6C!;b z_C`BIFWg`o4hvm6%B<$K8 z!`EH>BgTP|&1bvJ>i-o$Vg4K0K;hFz*cUu!<@x`W)t_N$#NXT8QGfra*%jL}`g;fOnfn0}$@ql3NOZf+Nio-7)I-^b+ER#q0j7>S68 z*zdC0syk@&rjm16v`tov=c{f>t-|K$VFyfWi*C?Ur1ejFym+%2x{5fjhj}{Vs^||Z z=|#KK3mgbjCRJ0WUrrglo3b$GnzwWDuec`VT%$QIv5sr}luIb$;y%ep4`le)8uycO zVuOO(AUAVSSh>iJmkDPOb9aPt1TnQnERG>&Um2K-Z*e>unn<8Co!`sR0t0k2*$GqZK(rq5p-3r-g_8 z6!zV6&5h2M<>;sF&AR5kuJW>bBAfPgb}NliS*`5)eFwwcj~T%kan!DvT{E)-bBTs6 zdvCirx9oDC?y*-7s;dp}_cS`WYA@A0+B$aU>_6o6?SbWr5B2{XkuO;PnPayx+{!Nc zi}gortXE#YkxnoJ)-ki68$Ms0Kc|T+C|cR>vSas-&9oWCrH!512jZV^z0yC9?lRZk z{M4|vQ@dB3b8`X!o_L%D;B`Frlo9}?Fq_)!trtp~XOIMcwh-8|jd~IHPwu#Fr*Qkx@^W};@-4zt9hDWGk+t7x#@kN!k&`?Ok_|aAX zd94q30!WCMh-(LsU(BZfU<`W=-vf}_sBi_4HS9eK89n^U=?wt3$uc`np!KWGGijMn ze~&DuBNYIJluQB;$xYAx0e~rMjs?J;n|1mm01H|<4S>&M^#?zMY{lcZD~+(ii|`H)p{Fg=$snG} z$EPbntQqrBMWcYFQ1tTw*m!ySzyY!3{Y#}NSY@SWXTWfma?2+#zzPon-w%dF$d8DP z=-v8HY2OZ_RecC5)dXC2j?u!x!VpQy`dg%zWOIC}!a_+rhe;JZk(}l*Dug#B98O?` zXjYQJ@rx3cNs>4LQKBJ98pnTB*e3ar!x|OoBs{gRr%)g{re=DIUP{iW86Cnqk`wB{ z4v}$)Z%667T9W$k*(+7Gc8+wD+HiI7S-Y%UOL751E+T&*mspzgTH>25NsE_p)efIs zaj*|@uKcV%G9c6qWSMd|>~6XEdm_{|a}q)HZ7CC?8^j5i6#RloqLUEh zRjDt?lj1B)7XO>b(-^c})Hf~sM_BYxp)*A5*aE0K!Wx0PbGW2#Xv$yc2vWc<1i2?r zjY$Hp45I7k!4Q2-9178A1R+>mlLMG6rw4(I5i>Da?2YBE7dk-Rsl%O+XL{WOq_uAo z7Hjbv)=;Jmb`W2(g|H@rS%OJZ9yU%7odL0(*SXkEW;{sRIRq`u)}I}O{W YpI55%J&FD3{|GK9;1Kf;(^b diff --git a/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png b/gio/gpu/internal/rendertest/refs/TestNegativeOverlaps.png deleted file mode 100644 index fb50427e4abfbeab0128c74e31584ab47426ed58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2_SGb59q?kczms2Mrm4yh97V`seb< fF90&}frjmj4Gj#8E6=|Z1u64%^>bP0l+XkKZXy*1 diff --git a/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png b/gio/gpu/internal/rendertest/refs/TestNoClipFromPaint.png deleted file mode 100644 index e774064b7252ed8a2b0d3603e4a8dae6d5bfaf16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H0wgodS2_TxEKe85kcwMxFEDa4IB>9RxcqPK z|6q=$=$6p9XQ9_#$*4OtFfcN)a0n=nzyO+y4jOpvWIr;dbeCtY1xb0j`njxgN@xNA DWRD?0 diff --git a/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png b/gio/gpu/internal/rendertest/refs/TestOffsetScaleTexture.png deleted file mode 100644 index 27da90a62bae39124015a9d92b6cf799ddc0801c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 402 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV2t#1aSW-r_4f8cFBV6c;~(#9 zHM#0Exf(?1rcGA(!D3d+vHM}u-S!L5ggKf{=4_N==}r*rRxp|-e42H7DtrF-&o7VP z`Two@j^SYyprIhZ6L9(M)1_a&otx&h>GzyBF)FK{d&X5qr07;!d%o+Ryu0_w=9!l* zm#Mv-|Geq_fgPL<3>*xM3Jfd_3<3;H4h%hx+t0pVJbTiVX{V=hDfbrks_nd!|BB7v z<>z3DQ;xo^mUZ&W4=cb_L$n^?x%*AkpTFu3SDeqi?~~dOKPj};w9Q#)rh2o=bKCk! zQ*W!pf3`f{zx?T`bjI28e_0KtvbP0l+XkK0d0`w diff --git a/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png b/gio/gpu/internal/rendertest/refs/TestOffsetTexture.png deleted file mode 100644 index a365312b675922408ebd77b99e016f3ccfb842f9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 386 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVD$8KaSW-r_4c-+AG4!G+r#y| zEKSG651PKp(Bj-z!PnptqF~7|uc|4h>$R!CmBpMahm>|4IF~og;d|v5lX|O4^V~FJ zPM~oh5S?-1-Q&>EEj8a}$oRin|9_5JIseJD^}8iKjq-Q%|G$30_(DFD00WZ)0|x`6 z0s~6}g8)PAg?BtP+g6=A9=iX-RIYR#HD*Q=L;X81mxpe$Sh>IM%e*=B)vQmyOZ$`4 zJlzqYjR|N*gYLC=9AC1c%0&Lu%v^K)>vj1Wm2~lwAC>|w+;sE1-hzo2qazs}mll-- z*Z;kqSX4iI&$}{nWzTifnKF)@Rrt?{=9K#R-HM&vioH7}9;q`hLfr?l2*e=(UbN;v WO>~O=P;b->Qt9dH=d#Wzp$PyHz=C4{ diff --git a/gio/gpu/internal/rendertest/refs/TestOpacity.png b/gio/gpu/internal/rendertest/refs/TestOpacity.png deleted file mode 100644 index c7f7d5af82c646bd8c649fdf6cdf9403c78bb0b2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1993 zcmZXVdojVSOgJ?pW~YnYCO=VoM>VcX zF(J8Cq}n9X7`cTV<5Jp$5yt7rWzvzmGp%#^=e%n@@AJNEy=%Siv)&|b#Xi($>_I`lab6#^kba>vZ=0)d%R-?XNHHp4xU$&o!1l39ou0t!fq?xGbpu( z_1A(ZZbp* zNp!=!4jkH+2|tCF?r*0U>ZSg`5lZ0K}2G%Jo#0SMAS{q__oko)gq|72tI&$|7a zG>76^PFPiWuC0d^tNpDDZ^Y1F)1;Iy{p)6z{rIUh%0=S_SqN2N-u9fj)|;{5+dkCp zWKzs;a(Ov7_VqJ8CJ3m?K)>HNTwJTNYuTA*e4%Wf7n5Hf1*xoAkLf4NZT;Z0 zB%J3hpRd;o+zl@JsxzK>s-pG-3xwXZrNCQ|r2q&5XQhf{!x zgsDH7c!2W_ypwE-1un(^> zs^@%S4G>`ftuik0z#AZixKZppRteuBLGZS*X*IJ*#m}#{08!<&0NnzGwsR+O zz5%Poysb#Y(X9`krJuDc(1(?d%THaBO?`knR3(ssw2NPy{bCI;!tLYU1#%GR)wk>S zMRAr_BrH7`lXdHsM(qx~wd^B#h>Hp6c+o8xkjt>)xYxFMtvF}mgc{4vQlTAKlw|Pm zt6!lpPXpL>uR;(sF!**^f64UQ)=UCgnpMFlR)JtOamFsx@dl|w1&!-SRqm_}l3e1? zyshvTn2$2d8Lob;666Wii4za8Xne0tZed67>WU+V;mqTCWfm%F@r$3Y>x%Vx?}2^% zUV~!{jOM5gIbqo;DnplLn46RCo^>W>CWa^ucdfGTmc}t8_I&hx!=o+PH0avy;UkY1 zs`up*dXOqXE7U)3i=Hzj!>Y0F{6dXye^zQU?Xw)!(ILyCD7@bZ+ z7z|!07QJ2Ty6=!Se0jzxQ~qfq+Xi@qcLlWY7K~6txGE_HVF#bG(8hymoRfLlj3x?vnD+~g*0h&roCqxMLR|g}0diSfZ z8uFBYeW=X;fgczXBP5@c?;E;HU&UdZlej~&Ibq2`TV7>yGot@Uk>fbcBqjriFkTG9RZ9f~3o){vryrf~4-hT^OkFgF8947=Bkt zUow_7cbM-c&Hp_q3r|gG@x=i_B0c{Y++0>Iigx z{GwvcQqqmdDAW2nz*5#booj8GytRJ+cK?<_AEO+nlo6x4w)HH^%!kCGmeSw)Og0)9 ze%Q9sfi+Tl9MbsP58L)4{B-lieXSCyO!*OOO3_sVBX!mK2rdeb*b|GFX;^8rIHbl@ zb0-yyv|hxCS$h!Xa!e!grK_au$rm2I2l~@0qo#zM0-%qov(619%P>>lSKDxE`0(lA z74Ep+I;{|_oj20XT1)`{X{P0c97japkym2MsS!fCa@OFP;lZ1|h__(y9nL2lVP@y6 z4*X)|CM$5#>YQy;2a8!-i^EfE zNy81p@S?Z<8&hDLoKsNCl3`wvxi-eJ0_kqG@Pjy1LiyA435YO7o|f~Hoc$i@7bKsI zSEhq7WhUPXyX6l``iF@8P5Tvu8VqSSNN&G06QNK_FyrYn?SAz6d-$K^(0~v`44oT) za3_=Z`BdIlnt>T44Ug;&ZTBTVtScfETsagN)t>&{&j|>#duBkh=AZXtH&eY4?~Hg> zTDemXSq&De*Xx=}h;PoU6y(}=XLx)UCpkMhRNL-&yytX-H}vF$M7o~tSUN1X;yisvtE4+ShE?)FtPc3331qq%;&E~ zE$+WPYx27KX1hJ5^5w!eUzz(mUItu|N92p5$jm(MREi}%$T|APl*EhPemvv(35!9C zq~7I0mWW;=w%Aw_oh!dSF^Rx&CXh1m-jQjw)?BrH%MHa LJRKkIWnTIh^p|fg diff --git a/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png b/gio/gpu/internal/rendertest/refs/TestPaintAbsolute.png deleted file mode 100644 index dd0976076b3fae51cd2b498c05831ba32966c5bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 651 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrU<&edaSW-r_4dx*tdKwvhlizU zS-LE@Z5k9Jo-o$1aO>7Gvu@n}QSc25cXx!U!nS{|I-))X84G_*U!W-HnBvm;EU#Nq zpwn=sqQd6yzZ6oP&b={x{!GPOT*Y$|5(;8_yI_BUgGe>wZIk-{w)dI0N;WW_U`Sz@ zb;6%{Meb#WtltR?t*mRL9TLyV9`J5ptl%_Y&}U?Opn8DeRPO(Wstdk-`@ijH_kv5) zq)XTWEE;&a4H%dwFlNN8<_+Ur@J_J6@N^VU!Y1Vhe_gUIKa{(--aeErwPq*t72E&o zQzLsAd-fl`duEFqv&4Kxk!_cnMVVK;t~w;#yS$)yqRKWh@@^wDHaLIj)Z)`rzeS7$_SGQTi zeZ4Iy=GA}JZ!Y$EcwALI+?=63-lXAe!)L!WX$;K`!G5dL81$L?tXIxrJX2b5L-Q(k zg7Sl&%PYkXl(C01_gT!WV!Xp=@zCWJYk~fQC6iZ(9{Ap{vmsJFK)c~-LpP&)?NfE* z(C^GFF3n3DqIt!nY|2}HGFR~0$kKCYEv z*F}fz4fzKe9>$u@m%NwpQ!s+R!oKRxt=WuwtWEyNphPKh^di_xuGBI1>+vRTTy=3G QFp)8My85}Sb4q9e00x>7!vFvP diff --git a/gio/gpu/internal/rendertest/refs/TestPaintArc.png b/gio/gpu/internal/rendertest/refs/TestPaintArc.png deleted file mode 100644 index 9ebbe5bf42e98ce685e9e3a90285e621108660f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1747 zcmb7_`ynU33OjxJ_&I&K}&I4uj?%sr;dkZVhY zhA2fcO~P3_=NP%=x)Y_aLgmsy$NT;T@B72^JU=|o&z~pT-`5kPj8_H#0ORfD7PynF z{|E`+@h*ojDFDDryxp9SFv=FoL$W9o-QH9t6_LE09I8Kk^O^$z<;HbFwe8O8h;0tc zAnXzlvSK+dMrt|GGp4oX&p1TnjZ$T!DxqOb)^#N`N}@-;)L)CeQu68dr10~@qr{5+ z6{5bWn0nF3$Fw<*h!3Ab7d9#%I1cwxxt`D9mR?(=5}J z06#Si>+#Bl9IxsNez@HRlIjKI@9(XS()PwH5w)`jnl}$x&j=Dk8VVUAG6l`nc#b_DTX-mfEi60^_UwAaC-Bymze*C6)P+^g9NXFz?USpfW zd_4*#s53W*^x7uzY2qp5R2d@nH+f{F;l6&lDj_Z+@5&jSghc;0viqQWa4V|jy#Imo??LZt z{4*^|{4)b3JEI};vTIOZ>F9Hr1e<+@^twpjwrSOF3}T#*C&Ui|*;Q~aJ(AO_8jyx3 zrr=;S>si46Us2bTV~M@2eR4wFJ<-uUK5QY2BsLSkl8iWi5H9pYfY%Otw!Jk%svCFz zXlGHf?vqW}$IoPc;5!9U0Mc!k3;j*Q&#i8L)&3(Ecix>tu!iXa`aNqUpi~$T|K*&PD3&J^me?B zCqImqC$3btI{SMV)Bsa1OnJACE$HmB66G%ZiwW7vU?W^m?=tiKKm$`+n@L~KMth?e zmTC5abgZBhRN`>`2iO^9I}#A#ijt&tAouWkEf3P<+cKAx>+s!06rIKWM3cC4k^4k4 zg+?5Y-rgv`YHKIO6{po86xn$|@) z9G(^ScQL#U77LDr1bxxK3qj_&;&F9!`w2886Z$JIc)IT?nptPtt8(s7a$-@b77w@1 z6I-)29gUVM#0tvLi;8i6fFxpBIG}(<`GHE2S2ua+iMPUrhkK9T#?uVILTLUvxt%>T zP?3qd(@|OuCyDpMJ)j*cG}e|`>#l_4dbrRXLqtSPQ$ED+55MEJ?W568;19jBss+6X zmpia4$-dik!NL$m_&tWm(+Qp*mQ>DLZzKkv{e^S~WJ8mug8g*YAJQNSN6mni>Ad|4 zsk4B|k&TH0uE(2n&*(l%X*yQ^Gs)>&Wt{wytmI=jNeF5yxF6QD(AqiBw0fsDp5%l$ zi>LWSirnL4Ooo7ulK$O}D0If~ajbB1NH7dXo}aTX?A&0zUQ2)B5H!32EqpYhI8ew> zoRX>!0wAe*?$bmyoCd>1?i%&MJZVf}Pn&7pvR+GFJ(KaAA7Uu_H5=Z5s(%oAT7QKK zKDLy~4gp8dPnavF-{0%asE=12+$t$9viqzx`qS8x#S^>&dWzNR==Xw!c0x3(p-FTT zWW~%^2o~yR#s7XX{VX)RW`|ZuPBgl^>zv6fSLYUDqP6n~Oj4OwYm--HocVbC?w*bN zO{NLDDUTWj_}bQ>~4dcd#5gH+XrL(^&KilP(_r-g3&gD;#QPH z5+Dsd+8Hw0jHm&ui)TzX3cJmMsk2TbCzuzDgn__%mHrEc8WuWbIpj7wG*_?Mnyn?U zX6N)NS)@w?AxC;UG74{f;xM%qPfE9$*UU8j%kwN7L*r8k z%13cCXb=)-3+t;Iyp*0AIhVwoZeOYDUEEoE6~h?(5dzLgV}c Dn_VCY diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedCircle.png deleted file mode 100644 index bdf1fced94025d6c64c057e343c4e6c2138ac6f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 276 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSw>@1PLn>~)y=BdHC_td?qI^zC zH`lavp5Y6#g;=Vk9ZyDNC^q#689jF}oAA>k@0~9bP!j}vSX^whIwkVtwn??B;^j*_ zy`D{#(OUWZ^jyjR57eDERm$#R_{FlJ&b)@Pr~XH`|6%b19kK7Z_bq0aB{k{l_J`aF zcl!U&WvF5=5OR3W;Kjfs&=B6hpu@y^!H8jg#Q< evziF{-&tPQ6CSqfFYKEDQtj#L=d#Wzp$Pzp@l|gC diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedRect.png deleted file mode 100644 index c1dd7a0a37d603e2d32bceed462e05bbc3513272..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSEuJopAr-gYUOLFhV8Fq0(D?87 zoU9Y>24_qZUa0});b2XOu(Q?6M%Po;_hr=u7?>OwI2afe7+4w@aB&!j10Uvczw%%* T;uAh~3Z%i))z4*}Q$iB}f`B57 diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedRectOffset.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedRectOffset.png deleted file mode 100644 index 542f73f890a88a2f8d6ce21feb3d33a91f5a2c1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 435 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV65_VaSW-L^XB$O-opk0EC;7Q zJalJfba1*+NE5Tsg>wu&`jHQm_qCZY9^q8*V<=?m5O$cu@Q6h~y}^Pgp&j4*CT2e? z-al3P_?b!|>Ayv{A^F+vipOdV_LhvKn+LQt_osNlnK{?ndXEZ!u<;|!Im9`d#;SJ2 c1H=FSe~EN%mM{;u?LZELr>mdKI;Vst030HOF8}}l diff --git a/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png b/gio/gpu/internal/rendertest/refs/TestPaintClippedTexture.png deleted file mode 100644 index 1af7b0dc7d65d194b33d29424128284417cda2c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRS-#uL%Ln>~)y=BP97%0(t(OSt# z>0(y8_2Q7I*0_ZMFWxvT57{KheIcr$<%CL(ZJuV&wMv#f=VWPM5MW?(VBlb2RA691;T+&!8DxKIZcA_c$()&A@>9Rv%G`9< zn)9_uLujyGL)ol3WhZL`^j4g9VVKLd!S5i$tS1FN>kb=Le>L9O$MS$5#Vi!gf$eiz e7#WBM^{<&{u2J@}ve;Dz(&Xvt=d#Wzp$P!3S6og2 diff --git a/gio/gpu/internal/rendertest/refs/TestPaintOffset.png b/gio/gpu/internal/rendertest/refs/TestPaintOffset.png deleted file mode 100644 index 82394d578be43e01cdda1dd8107aa8548086b8b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRS^F3W0Ln>~)y`(6_V8Fq$aq<8E z##6U$HRwFXFdHZh1=3gEC#dgDdHCtubBjg>0R|=q1`Y;B1qPM|N;wSpfWduM~)z2wNtV8Fw?G3AeC zxx}~GTGEGJTAfU3RO3D0$HI1iL4tuffq|!i(SU(s4g*o(fV`f<8m1IRYuTG1^`5SN JF6*2UngI9pC!hcT diff --git a/gio/gpu/internal/rendertest/refs/TestPaintRotate.png b/gio/gpu/internal/rendertest/refs/TestPaintRotate.png deleted file mode 100644 index fe15d7dc415927b88b4d0308728629bd2ad42db2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 871 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVAk_=aSW-r_4dw2Z)Hb`wvX;E zf_sh~%06<+Ys2Qv{?m;nTsn8}T+oKiJU5p5)Rs+anmhM;RHUa=tFYjq&fl?{4=Fvr z`|_-s^~ul8KiQxCElb-xGv8s7iYEkR?P_pMPh=C_AX*?)5Px8nu@SS|28j#050n(* z+!@=?ofD~U{MxX%A<^{5sU5W~tm#?1zJ1%-5OyH*z-Q}&;o*&ao6Teot{2%LU+{PP zh5J3l>H6=d?|R!%ci=L6bA2ph7n9lClY8cUTiS59;q>{If3q(ZhkdTNWzN*c{7#7V zzf~QJj`d;QFASd<-!^P+n11A7`1U5-^xcv65BIUju-v&K`fGOK(>3PF_4hd?SbC0$ z)RexNae(Ddi$3cc)|&YaFRmII^nSE^JDYLuCsiFO12@LiYQc-$BQzEW7sNSSobX`v z;{#J({p8#pb|CRUF0=FczS-Nid9;d*f9$fbP7Xi3on z-i*@ArtRa=m`r@lo%ecizt6_PAXs~xQL-BlzJ(eeP6YgKRR4;tve1_-_z8S__ zx2!&JD&7B+U&a<6-CDsfKpXParqxN`IJbgfUVOscE0@+YO_vlsII(l7e#6}!t^|uu zag9${H-tDZcAvXAJL_fl1B=D^OyX+O{-qWyKDTbZR9C`jkfwCWTxNB(Y5N#%0X?#J zx{qp@S>rtun;*BC`1Z!E+86p@LF4b*K(mb{d;C}YV~Us?5d5OLobAgV+mt`91~Upi zgr_cECGO_4f}u?9iip()+dY=91*Yrl4t&~sxbHz*t;DU>n_n;g^X$Sl&MiB&4;1m1 zv)y5px&3LXzu=dkxo`FV7j8MNzBxDHv%TJg3a%H`%k2M$KKQe^-Td_7Wpa<_O$uG` zS>Ju@DYwbf)XSHjQP%nM&}(Mmfr-z*EUOo_sHs*^Uhw+jnwy#pTR*+OF3g{F=H2N9 l{<~o*6`o}AF_xXz&kDZodOGo>FE9f!c)I$ztaD0e0ss%En1%oV diff --git a/gio/gpu/internal/rendertest/refs/TestPaintShear.png b/gio/gpu/internal/rendertest/refs/TestPaintShear.png deleted file mode 100644 index 6d1a4c9299357c1329d0f9d284ca731c79468069..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRS$30yfLn>~)z1GOtU?9M9@U6{C zn|x8nR>=)a(|?y_`JWBnA8h?jh*8ObrK7>##bE0nRl$w}mIZt|jj93&#NqNlro(|n zNuZHq52uR)lb{7Vh&_i{FoFq$fO6*=Ids4XBn?vg!9WDffCEAuV6-PUs}txpqQQ=* YjFVhvKW)oczz)*m>FVdQ&MBb@03bU+NB{r; diff --git a/gio/gpu/internal/rendertest/refs/TestPaintTexture.png b/gio/gpu/internal/rendertest/refs/TestPaintTexture.png deleted file mode 100644 index 95bb26cebf9fb8d2f5607ff7b1a23eb027d6b161..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 383 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV080zaSW-r_4bZo7Ly}G>%;9z zj*9FauS>W1ywrRX9I@oQrjG7ct8JDI0-Q~oZY-OvnfBOwI2afe7+6p^2kebgJ_hFtefu?g zjoHaByU%@Jmw)o1w&ngUmHRF}FMMI$(4V@OA?knP*7N38XRY}YW_K}~*gaZPyYIND zRI3u2X$Ye)@J-$jbl3k-kJITAd#ys*wB+Y%WN;>fMj+5~Uuk97YGE22Rm9PCw)Q#p;y3yh4p;IDSlALgS zX650T*5=h^-_pLF>#sgn{_eY7LBjKQd&|$A`<~MF@T6Evwn{@WCX(SaxWlA2KP3Cb zi}zy2JGduo-a5P5TuF0}6z~4H@Z$GgDv5uj4m7hd@~yhxDfhX8pw0E&xehn+)?4?l$2UMV+t!kFjs{%iBj^^E4rp1ZW?J-xR#f;+C;{&xMLdw0tBA6)sq>EWE6 z&ex0nizcc5`1WOPt=WnEjrCbKe3@EA&-Kd5g?<-eHLQ|+@VBaC{=@RV4{6zqhju=k z_2R|p&+X1nM6#~J@0EI#IF@7=KBkm1?Ek|rKgaTp8PGZJMBhou-daB|SmI&Ilgf^}Va>VP4iQ~$X_;L%Z`*eD zFoc+FI`Kp4fcsa6K4pJ%CKH#%?pNhp*u-4ttz!u=F*>s7#O1jlMF*Z-u25U9?7y99 zgP~NbZ70i?PMf>j4z7!3w)EO$0+o85GWm38W&E3?6V-bea{AoLMS86gXP%hs$Dk#( zG~won$zec7=}E1L>U$Zw+~#h`dg7_iAlkbt;p&OW?m(&BNu^5uc8n`Bws7u9dva3S zq1a^6qCjguw|TF%UFY3l3c2`V*P@GBGVb%VwcY3aVJZ;oU8fn-w!OAz>4)ZT+gg()9W=OxNk@`f_l_4BVyEFXia9gSDAIY@g z{>6^zm)+-mVo4}A*)^xSD*KX1<-HUCoT5b)_L$jrxmhav&t%#Ve^Dd-iDo#%H)W}( zeKx7w2Qp1|iQDUFN=0_qJmn6^Fxezjc}lz?)lFG@;&OSW4Qnz|G(UZDXP73~yT)~1 z9cO}#RAi4$t?+>olT8wpztkCW-IP~PTt1)aLYT=WnaW?<47o1KuO}|Q&r~AWdv0#+ zsf(2lekRsDk^a-c{i9m={CtsZzngtOvBw=UyVu2km-Sx_{|)dd~)y>XD2!GME#+Nkr59vUe;~(RN zT$%Ku1Vp-=*^cYo+%2gQp-hfuLsy^dBzX}py`Oata!-Ex|Nq|I-QVA-0PR8tKiKbYx{|Qq=ENy;Jj}Es z+p2iNKC7#At8TuQ;jvBeZeQ^1gB%V^*ZzF_*lkn)B+-?D_kYct{AQIm!!7q%WBX~M zqMxp;S+7#ec=mhp!#g6p=1)VW1~=aNdi?2ru`7Qj9@(-rsc4scdC@A(OWAL=++{D{ z{L_?D?#Nj2_2`phx0pW|D8-&!skEFkYt}l3kmgzTb7oxoW^MleyWr`U9`BFNb9&$X z`Ba3l2E);1nzMK~8&1A#xwY)zw?C7_&im-^o830)-P!ZAGJl$E`Kh)%vTHV*U+K;@ znzM}d{!Y+}{ndJJKH~}XWpjfvK1*8(?C#LasQXy>dEQL3>%RH=K9@Jn|7mo2_IWjP zDX|6*X%TZ-MvFK1P6!5nxwEAzr|Rd`kbB&7|6VUjdcEWM+srjr*X%dny63s`hN?Ht z58dYd`m1=<_xKzA_OmYn>woT#=kPoJ@7so*@AAd_{~xY&JZ~M~=GVJ`;mwpkOdBru zCA_?(FSA&4^66P~UI8uITyOTHFz@G=M8zAKJgWZ$i$0#&V!Pj^@Umcof8y$&p8J0a zZ}Ob7ahXW5(~Zy9rcS*an7#YkONKRXf4{xIA^4EiQQzyK2~K`fuQB?yGxYpyp1l9~f?mg)$GgHCScy;IbUR{%9y~^&YGKglG2;?3AtNrL&Mt0qsc-7+!8+-m|s91iK mpP10_QBo2;{Se1*`2SLTQQg}-9^X$V4C!>4V;`4? z+3wh)rF&)R(kr=|ccT_BZN0Rx`SsP{=}9IMRv8~7oOfru4BFMM!(=VVG^=I%3B_Qx z=1#?#Ovh4uWC|v*TYCpoeEqfN@=F83O{zD~e`i*z_e+=Ff8YAPzq&u?HI4%^j5CxO zidho+7|sYbSTY&7Gdv?+P|c>H*{SA0K*W!4RW$;QGdCa2pKsT3;Mn)yH;=w|yKuAZ z^=p06En$)2FAaA8czf=DYwknK9R`y>pXK5HJuhq4J*gk`Y(&kzB;@utUr1EU<%6woqZwi{!~}Frwi76(~A7_6(9$0WS^m&-HiD*?-`!!>30bzt@%9nXwt&%8%G_OW$ry=DQVl zjGt+mzN+1&Rr|5~%Zu6fYNv0%veWNGU&r0r=b?K}mKZF!$T~sp>!swlh!gMT$j|@% zaqT^hNk_xeT;ANDE?DTWm{Y;}x%Sox!KyE>XZ;VY6FSKM`_*iLbN8onuo*nu-duRo zt(c9&J;_4p@I=YJfVt;>mj9o+E&cIJ8<~p_Px!qtU3#1Q<8cQI;TB`3M}KO~OV)_~ zRgaUd;9k6&&BMJQXy$vJ2k+bqqklxsi#w6>QTQd$^7HqnADv&((>Nh&k3frCTO(kNjpNBQ+AAZfUFXoi8vu|NlQtTlAfZ Ss`x`7hr!d;&t;ucLK6TGWzwJk diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedPathBalloon.png b/gio/gpu/internal/rendertest/refs/TestStrokedPathBalloon.png deleted file mode 100644 index a9a57b28ebb26dd6331071820e07c7d09cefa9c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1977 zcmd6oYc$&l8pi+sza;85NUJV&zl;*XP)f9Dn<6O@gbw49c0`LRQC4W1Myu)24k}_) zFkzxa6|0I}m!!018Y3$Noz`9Hic9FttA$hnSbJLg$@=-aELS zJNvYKn$jw|=XiObzpGN^YR^uq0PRml3<0v9xc8V=01 z0WYZFo%aoOW?UVh89Jx%0JK!F6|4#Nkn^vs?vYtGYLCHppbH2(Fami{t{B`jJq02l zZQvJB8(IKQ97vj27W4{~r;bjwB1(s1HKx&gBn=!>NRez_0>-S;cIe#41ALXqZH+dy z4gV4CU%gAVtV)SlB5)qG4BjO4y>SY>b4~pML@#pNYHU7jZ9UObm7$~H3mYg1jK&|2 zNyFh}2W8D9GlGY`4ozd@==91Fn2q8*zHHLqqx1JCewa?4qLLqG1qV!j-4i|}lqN4u zg~jGScl>Hb-Y?@DmfQK|S&&>E8S2QHwZI$~msE|Ft%b8`5!JV&6b6=0K4G;8|1|d) z$`p}Gt;Ltl`=v)(W)24CNzZ=XTf;GHLKQ7S>rM>mk*zbABq`XD3)-i^FRs&VW7jqo+S54D_m= z9aqaa6**?YBCI)krXp%*dv<;K#)-&V6#YDR%SV90V%r^-?Qbdn>~k@^N(ci#)y~Qf zA4I2{@P7X;IlN_Y*@&k_ZpXM+9HaiOvR9Ik{pml)$qzAxueoh=H)|GY5zMuB+G6EK zoWU;%fp1k)J1{-8xsn(sb@J^iAx!V5W>qR5+$WeH;`oNQ@9S7*@|-DR~)9=9vz(N z|3;I{dC4_ZmSvSFHv0XclmgajSoAAvax0mH+vc9MtJv|)vZgDYt-JMr?l(0sn%SrJ z4iyc(%XSalJUS+JKQAgjcm^SS7do7T=`@CXp{@X3Bl?Q;er?h|49h6+`9f2viha0q zwoB!_qhaCOMI7}KSj%-x&r&VHzHSNE-X=WZFI#nL8b|La#KDQArNn%;P{K>yE$y|5 z?jOf9BGzdx>X0v@9QNtC^DP(C+a65waDTIzu#6cfYxJ{ZT!L1?#(zohukmKYgtB++ zWeIh;dAXBCw~nkyeR4P|#G7qEpUxJ7JysFc)|r`IVlFV&?QHip9AEJnQ@(HdxTvVw z0AEY#a0SJYWS05G!eXs-1C$66COH=+WOX~g;?{_7#Lg*T>jE5B$)z^aacm{dqqwCm z)O5xO{)OG$g!w}ef0h2DVNIHH)|?Fto-^s0U^^ePXgN*1^`@#JVW2feqU#6p9v zZ)#ktGU9V4P4|_sd@ieQ?NFQL^VWzdE?nZOt)k;f}#4Z1_%lk zUVXeZV9}(CS~Tbo`&G6Bt`!fUx)8{wP&D7S_wUuhR=ge^?Fqii25ytSZ56)cP!>%B zPX0y1XvB{>1I^vafEU-qsKP;acIr*<;CZ{Eaj%(sL8$OA6YxGp2CNO`yjFQC^?mM3 zg>tvy$u5VrLFHURV~e$}+%+lwLS~f;5t(xR50bnro=<|G2#%3rQ$T%7{|%2k n=l=qd|4EqtpWvDz0LbM)>6@0kG@JfiBm=mU1Z@3r^7VfKP&p=m)64*v2e2Q&5`cewEct(% z_kUv)$Q*#50QAPhApkqLlQq&Y4Pc?gjvtH#|64;jiyd};6N}u*8*$7GM#t3Iuf}P*Q1Kgc4!k9_8^IaU^?vN2iO82`s#@!*KJu@xO_e0`8 z?oP#0)0M!T04`3v&8f<2s1^$MH=+6xaV_elaDT&UqU{=qpme{B4cr}!p@t&>rTZOg z2)AuSLhXJR*Kv0;hSHA!)b4kjN4$NbBRSmfVjFiyV<`Cuki-3sbs5?qqf`Mn_dl7b ztqU@>Lqe$n$mM>=g_+tRp+o`XbU#c0^2q6a$3+?2GRBZ==HDM5ad$d~(vASR-S60# zxjkb99PW2)eW`-ica zx&6+RAOLRnJ1%8zzgoD*{wChwE<{(f9|0t|-*Gv20yQGV{VsmxE=G-Tp7#$6)whMU zA<6ws+{#^)E@+(tNE!S4xsp49E@%{hg!BGkT+N+87c>e$qWhuwaOj{Qx1!(E&j zkT&!0kCXP45G|73-^5n#B88%94j^^x?`I2l0-+r62)U?oB6{@vcKClYUW7kAN0g3A3)RI|Q~PZMwZ zP3D>v1Eu?&uqJ#vuO!~~5_8>(f!h6z03IS}!`28!LhXL^3E*0SHs%>?R|J&qZ(i)! z2w)PxZ;9J^jj?v>$>DzV3Bd7g;gs9Q1Qa6bkF@DxFNmod~%J-OVE0Rh}j(B96> zwJU;L?#F-t-2eTPxXrgR*G@e--H!nQI6g?+=1~BvGS#jKa=ITw1n@LL+jnKEoqBS+ zA43E{abxJ^47DqQ-0sH^0h|TkkHqaS4Qa<1fW!S5v18sQ$l(^k+7$tZ`!PZQj}YYX zB4O>+!{vUA5Wpn_dHkqo>0NfX+>aH;=FbW8xs#xF>fv-hRuI551o^z4xOPRr>3*yr zfa?izItJjJ#Pw4RxBD^X^z2)LydF$cyCUFrKgI~)IfC3iPEXG7pv=G2v0s;I5V0}dW)FZ|HXd!^j{+a)j0_geGM^z%p z{b+fU<{*Ipmirz`sR~K%M@s?B5eUG=;~nx9_;7~`=szsXn5pbXW zB?1As*wZm@bx3qS0t9f?ke!!80KWh@KO%oMNOV5}SL(b^AOIK7N93;psqROh0PZFd zz;OUqMD{~8Qr(Y00Zan;nm_<9-iz#qSR}h29TsaoOe6po+Y|Ih9FpCS4gy&1Pv0m6 zuw2?DV(IQj$K~3)i3HHo{N15pNOwOv3Sc%u*lkWFix>(txE~P@Y&=FNfFmV5R<6PQ zh!DUk0N)V_py%IOruJr$7WX6ak*)270_geqfXYY8w74IU1)lJ0{HO%r;@O0K&4MQP zBZ0%V1}+%saz8=UTSqmy9|;8De*Y@508aVm6~&{?{YWH$_5Q9Nl>nOGJ&~x-WN33g z5(&U@AF%+MctPqzrW)OkgaVjo`JTZPYYN_xxQ0NZ`;k}xn*jWtK>$7fc{(wlrqSwt zBo+W#3`yS7-47xGz<)XH@5};d;xm8UU|nS2#-iQ*AQXV(ekK7lal)UOJW93ZQ1Ul?EzXQ( z0Q>z!HTq7YhHv;Cka-)=zMZGT0X?fKTkzr)3c$fHnRN z7e+yZ++CU@%)?+JL;%|e^DvkQ83O(Ua5iBs1_L1i=n>{&Fd=eAlf2P0;;~>#1bbFYR=c?^y2nGpyy85}Sb4q9e0IGW!F#rGn diff --git a/gio/gpu/internal/rendertest/refs/TestStrokedRect.png b/gio/gpu/internal/rendertest/refs/TestStrokedRect.png deleted file mode 100644 index 27c1c918d51c4552ab8c6f19f3329c144f4147b8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 584 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrVB+_5aSW-L^X9IhpSGch+r#=# zaosCC4r|@uTbL+olBn77rakn6Lu-(;09S5TqS3Z@b3U7TnCx3Db8_9A(3UiY0yc(p zx)R^_e`g4;ReK#;H2ukgz5{(F`EpR`@9n^#wC=4r*9UY{mC*##8;HtR{6$Gzuq zD}IS>h&H2-f>JCG` z6nB3>M6EA)=qe9&EoS-19v3V*M7okN>l_x0VOja_@L+>ri@?x#E@WfgZak zolPz7i)ZawcQs&l}oRzI)CS7dT>fGH=Cy*;NaENjeBiGR$E-Kn`)I d9GEEn|4;ZVD|BmVX*H0;;OXk;vd$@?2>|g)!5RPn diff --git a/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png b/gio/gpu/internal/rendertest/refs/TestTexturedStroke.png deleted file mode 100644 index 8da06b39b2a953f7c9df1f377a08d1998fa326a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 692 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV5;_XaSW-L^X87XpGcsL+sE}+ zT6kw#6i9zDGdaYxhPR-2TvApt?byD8k_4lp*tDStV^4{pV^ZmrHZ2OqD zbgP7W&inuC*>#JZcMGOkinm(-JGp>y%Vm}HtJ-B2r(e$r>wjUG!B81$`=G%z|F%&U z!}(S0Z%o%#+~!-%_F+fW0mkfm+fuJEY?)wqpG~f6b@~78rR976&G>A6UgwD@!#Rcr z$M&$w}`+ZFR=b^iVT zXOB%aSo6EYdbJNT&VTq?*Yl@Q(wL!MKf&-G6U0&-aH>&kIMADE4DT-+J-5-Fts9PQ28RFtub#FH?=pC}3YZ!h NJYD@<);T3K0RZ9(2O0nX diff --git a/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png b/gio/gpu/internal/rendertest/refs/TestTexturedStrokeClipped.png deleted file mode 100644 index 8da06b39b2a953f7c9df1f377a08d1998fa326a5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 692 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV5;_XaSW-L^X87XpGcsL+sE}+ zT6kw#6i9zDGdaYxhPR-2TvApt?byD8k_4lp*tDStV^4{pV^ZmrHZ2OqD zbgP7W&inuC*>#JZcMGOkinm(-JGp>y%Vm}HtJ-B2r(e$r>wjUG!B81$`=G%z|F%&U z!}(S0Z%o%#+~!-%_F+fW0mkfm+fuJEY?)wqpG~f6b@~78rR976&G>A6UgwD@!#Rcr z$M&$w}`+ZFR=b^iVT zXOB%aSo6EYdbJNT&VTq?*Yl@Q(wL!MKf&-G6U0&-aH>&kIMADE4DT-+J-5-Fts9PQ28RFtub#FH?=pC}3YZ!h NJYD@<);T3K0RZ9(2O0nX diff --git a/gio/gpu/internal/rendertest/refs/TestTransformMacro.png b/gio/gpu/internal/rendertest/refs/TestTransformMacro.png deleted file mode 100644 index a9cce29f429254a6b654cffeac6b21c695a64cc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSi#%N%Ln>~)y>*b6!GME#*xM3Jhg0Y~q-*!_43UEFjtcS$|k=K@@=b iOid@D`> diff --git a/gio/gpu/internal/rendertest/refs/TestTransformOrder.png b/gio/gpu/internal/rendertest/refs/TestTransformOrder.png deleted file mode 100644 index 720ca3c702f872d89276bb5c415f01b305feafe8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRRo-U3d6}R4Aa^zxA;9zik{lDJq z1@}Y8*#~zokY)rbfPs>&tHhtk%E^WNTKx7%y>J7A00WZ)0|x`60s{-(ISdGpFppV| VooT@>)16;Hf}XB^F6*2UngH+7E9?LO diff --git a/gio/gpu/internal/rendertest/render_test.go b/gio/gpu/internal/rendertest/render_test.go deleted file mode 100644 index 54c8ca1..0000000 --- a/gio/gpu/internal/rendertest/render_test.go +++ /dev/null @@ -1,506 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package rendertest - -import ( - "image" - "image/color" - "math" - "testing" - - "golang.org/x/image/colornames" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -func TestTransformMacro(t *testing.T) { - // testcase resulting from original bug when rendering layout.Stacked - - // Build clip-path. - c := constSqPath() - - run(t, func(o *op.Ops) { - // render the first Stacked item - m1 := op.Record(o) - dr := image.Rect(0, 0, 128, 50) - paint.FillShape(o, black, clip.Rect(dr).Op()) - c1 := m1.Stop() - - // Render the second stacked item - m2 := op.Record(o) - paint.ColorOp{Color: red}.Add(o) - // Simulate a draw text call - t := op.Offset(image.Pt(0, 10)).Push(o) - - // Apply the clip-path. - cl := c.Push(o) - - paint.PaintOp{}.Add(o) - cl.Pop() - t.Pop() - - c2 := m2.Stop() - - // Call each of them in a transform - t = op.Offset(image.Pt(0, 0)).Push(o) - c1.Add(o) - t.Pop() - t = op.Offset(image.Pt(0, 0)).Push(o) - c2.Add(o) - t.Pop() - }, func(r result) { - r.expect(5, 15, colornames.Red) - r.expect(15, 15, colornames.Black) - r.expect(11, 51, transparent) - }) -} - -func TestRepeatedPaintsZ(t *testing.T) { - run(t, func(o *op.Ops) { - // Draw a rectangle - paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 128, 50)).Op()) - - builder := clip.Path{} - builder.Begin(o) - builder.Move(f32.Pt(0, 0)) - builder.Line(f32.Pt(10, 0)) - builder.Line(f32.Pt(0, 10)) - builder.Line(f32.Pt(-10, 0)) - builder.Line(f32.Pt(0, -10)) - p := builder.End() - defer clip.Outline{ - Path: p, - }.Op().Push(o).Pop() - paint.Fill(o, red) - }, func(r result) { - r.expect(5, 5, colornames.Red) - r.expect(11, 15, colornames.Black) - r.expect(11, 51, transparent) - }) -} - -func TestNoClipFromPaint(t *testing.T) { - // ensure that a paint operation does not pollute the state - // by leaving any clip paths in place. - run(t, func(o *op.Ops) { - a := f32.AffineId().Rotate(f32.Pt(20, 20), math.Pi/4) - defer op.Affine(a).Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(10, 10, 30, 30)).Op()) - a = f32.AffineId().Rotate(f32.Pt(20, 20), -math.Pi/4) - defer op.Affine(a).Push(o).Pop() - - paint.FillShape(o, black, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) - }, func(r result) { - r.expect(1, 1, colornames.Black) - r.expect(20, 20, colornames.Black) - r.expect(49, 49, colornames.Black) - r.expect(51, 51, transparent) - }) -} - -func TestDeferredPaint(t *testing.T) { - run(t, func(o *op.Ops) { - cl := clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o) - paint.ColorOp{Color: color.NRGBA{A: 0x60, G: 0xff}}.Add(o) - paint.PaintOp{}.Add(o) - cl.Pop() - - t := op.Affine(f32.AffineId().Offset(f32.Pt(20, 20))).Push(o) - m := op.Record(o) - cl2 := clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o) - paint.ColorOp{Color: color.NRGBA{A: 0x60, R: 0xff, G: 0xff}}.Add(o) - paint.PaintOp{}.Add(o) - cl2.Pop() - paintMacro := m.Stop() - op.Defer(o, paintMacro) - t.Pop() - - defer op.Affine(f32.AffineId().Offset(f32.Pt(10, 10))).Push(o).Pop() - defer clip.Rect(image.Rect(0, 0, 80, 80)).Op().Push(o).Pop() - paint.ColorOp{Color: color.NRGBA{A: 0x60, B: 0xff}}.Add(o) - paint.PaintOp{}.Add(o) - }, nil) -} - -func constSqPath() clip.Op { - innerOps := new(op.Ops) - builder := clip.Path{} - builder.Begin(innerOps) - builder.Move(f32.Pt(0, 0)) - builder.Line(f32.Pt(10, 0)) - builder.Line(f32.Pt(0, 10)) - builder.Line(f32.Pt(-10, 0)) - builder.Line(f32.Pt(0, -10)) - p := builder.End() - return clip.Outline{Path: p}.Op() -} - -func constSqCirc() clip.Op { - innerOps := new(op.Ops) - return clip.RRect{ - Rect: image.Rect(0, 0, 40, 40), - NW: 20, NE: 20, SW: 20, SE: 20, - }.Op(innerOps) -} - -func drawChild(ops *op.Ops, text clip.Op) op.CallOp { - r1 := op.Record(ops) - cl := text.Push(ops) - paint.PaintOp{}.Add(ops) - cl.Pop() - return r1.Stop() -} - -func TestReuseStencil(t *testing.T) { - txt := constSqPath() - run(t, func(ops *op.Ops) { - c1 := drawChild(ops, txt) - c2 := drawChild(ops, txt) - - // lay out the children - c1.Add(ops) - - defer op.Offset(image.Pt(0, 50)).Push(ops).Pop() - c2.Add(ops) - }, func(r result) { - r.expect(5, 5, colornames.Black) - r.expect(5, 55, colornames.Black) - }) -} - -func TestBuildOffscreen(t *testing.T) { - // Check that something we in one frame build outside the screen - // still is rendered correctly if moved into the screen in a later - // frame. - - txt := constSqCirc() - draw := func(off int, o *op.Ops) { - defer op.Offset(image.Pt(0, off)).Push(o).Pop() - defer txt.Push(o).Pop() - paint.PaintOp{}.Add(o) - } - - multiRun(t, - frame( - func(ops *op.Ops) { - draw(-100, ops) - }, func(r result) { - r.expect(5, 5, transparent) - r.expect(20, 20, transparent) - }), - frame( - func(ops *op.Ops) { - draw(0, ops) - }, func(r result) { - r.expect(2, 2, transparent) - r.expect(20, 20, colornames.Black) - r.expect(38, 38, transparent) - })) -} - -func TestNegativeOverlaps(t *testing.T) { - t.Skip("test broken; see issue 479") - - run(t, func(ops *op.Ops) { - defer clip.RRect{Rect: image.Rect(50, 50, 100, 100)}.Push(ops).Pop() - clip.Rect(image.Rect(0, 120, 100, 122)).Push(ops).Pop() - paint.PaintOp{}.Add(ops) - }, func(r result) { - r.expect(60, 60, transparent) - r.expect(60, 110, transparent) - r.expect(60, 120, transparent) - r.expect(60, 122, transparent) - }) -} - -func TestDepthOverlap(t *testing.T) { - run(t, func(ops *op.Ops) { - paint.FillShape(ops, red, clip.Rect{Max: image.Pt(128, 64)}.Op()) - paint.FillShape(ops, green, clip.Rect{Max: image.Pt(64, 128)}.Op()) - }, func(r result) { - r.expect(96, 32, colornames.Red) - r.expect(32, 96, colornames.Green) - r.expect(32, 32, colornames.Green) - }) -} - -type Gradient struct { - From, To color.NRGBA -} - -var gradients = []Gradient{ - {From: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}}, - {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, - {From: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, - {From: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0xFF, B: 0x19, A: 0xFF}}, - {From: color.NRGBA{R: 0x19, G: 0xFF, B: 0xFF, A: 0xFF}, To: color.NRGBA{R: 0xFF, G: 0x19, B: 0x19, A: 0xFF}}, - {From: color.NRGBA{R: 0xFF, G: 0xFF, B: 0x19, A: 0xFF}, To: color.NRGBA{R: 0x19, G: 0x19, B: 0xFF, A: 0xFF}}, -} - -func TestLinearGradient(t *testing.T) { - t.Skip("linear gradients don't support transformations") - - const gradienth = 8 - // 0.5 offset from ends to ensure that the center of the pixel - // aligns with gradient from and to colors. - pixelAligned := f32.Rect(0.5, 0, 127.5, gradienth) - samples := []int{0, 12, 32, 64, 96, 115, 127} - - run(t, func(ops *op.Ops) { - gr := f32.Rect(0, 0, 128, gradienth) - for _, g := range gradients { - paint.LinearGradientOp{ - Stop1: f32.Pt(gr.Min.X, gr.Min.Y), - Color1: g.From, - Stop2: f32.Pt(gr.Max.X, gr.Min.Y), - Color2: g.To, - }.Add(ops) - cl := clip.RRect{Rect: gr.Round()}.Push(ops) - t1 := op.Affine(f32.AffineId().Offset(pixelAligned.Min)).Push(ops) - t2 := scale(pixelAligned.Dx()/128, 1).Push(ops) - paint.PaintOp{}.Add(ops) - t2.Pop() - t1.Pop() - cl.Pop() - gr = gr.Add(f32.Pt(0, gradienth)) - } - }, func(r result) { - gr := pixelAligned - for _, g := range gradients { - from := f32color.LinearFromSRGB(g.From) - to := f32color.LinearFromSRGB(g.To) - for _, p := range samples { - exp := lerp(from, to, float32(p)/float32(r.img.Bounds().Dx()-1)) - r.expect(p, int(gr.Min.Y+gradienth/2), f32color.NRGBAToRGBA(exp.SRGB())) - } - gr = gr.Add(f32.Pt(0, gradienth)) - } - }) -} - -func TestLinearGradientAngled(t *testing.T) { - run(t, func(ops *op.Ops) { - paint.LinearGradientOp{ - Stop1: f32.Pt(64, 64), - Color1: black, - Stop2: f32.Pt(0, 0), - Color2: red, - }.Add(ops) - cl := clip.Rect(image.Rect(0, 0, 64, 64)).Push(ops) - paint.PaintOp{}.Add(ops) - cl.Pop() - - paint.LinearGradientOp{ - Stop1: f32.Pt(64, 64), - Color1: white, - Stop2: f32.Pt(128, 0), - Color2: green, - }.Add(ops) - cl = clip.Rect(image.Rect(64, 0, 128, 64)).Push(ops) - paint.PaintOp{}.Add(ops) - cl.Pop() - - paint.LinearGradientOp{ - Stop1: f32.Pt(64, 64), - Color1: black, - Stop2: f32.Pt(128, 128), - Color2: blue, - }.Add(ops) - cl = clip.Rect(image.Rect(64, 64, 128, 128)).Push(ops) - paint.PaintOp{}.Add(ops) - cl.Pop() - - paint.LinearGradientOp{ - Stop1: f32.Pt(64, 64), - Color1: white, - Stop2: f32.Pt(0, 128), - Color2: magenta, - }.Add(ops) - cl = clip.Rect(image.Rect(0, 64, 64, 128)).Push(ops) - paint.PaintOp{}.Add(ops) - cl.Pop() - }, nil) -} - -func TestZeroImage(t *testing.T) { - ops := new(op.Ops) - w := newWindow(t, 10, 10) - paint.ImageOp{}.Add(ops) - paint.PaintOp{}.Add(ops) - if err := w.Frame(ops); err != nil { - t.Error(err) - } -} - -func TestImageRGBA(t *testing.T) { - run(t, func(o *op.Ops) { - w := newWindow(t, 10, 10) - - im := image.NewRGBA(image.Rect(0, 0, 5, 5)) - im.Set(3, 3, colornames.Red) - im.Set(4, 3, colornames.Red) - im.Set(3, 4, colornames.Red) - im.Set(4, 4, colornames.Red) - im = im.SubImage(image.Rect(2, 2, 5, 5)).(*image.RGBA) - paint.NewImageOp(im).Add(o) - paint.PaintOp{}.Add(o) - if err := w.Frame(o); err != nil { - t.Error(err) - } - }, func(r result) { - r.expect(1, 1, colornames.Red) - r.expect(2, 1, colornames.Red) - r.expect(1, 2, colornames.Red) - r.expect(2, 2, colornames.Red) - }) -} - -func TestImageRGBA_ScaleLinear(t *testing.T) { - run(t, func(o *op.Ops) { - w := newWindow(t, 128, 128) - defer clip.Rect{Max: image.Pt(128, 128)}.Push(o).Pop() - op.Affine(f32.AffineId().Scale(f32.Point{}, f32.Pt(64, 64))).Add(o) - - im := image.NewRGBA(image.Rect(0, 0, 2, 2)) - im.Set(0, 0, colornames.Red) - im.Set(1, 0, colornames.Green) - im.Set(0, 1, colornames.White) - im.Set(1, 1, colornames.Black) - - op := paint.NewImageOp(im) - op.Filter = paint.FilterLinear - op.Add(o) - - paint.PaintOp{}.Add(o) - - if err := w.Frame(o); err != nil { - t.Error(err) - } - }, func(r result) { - r.expect(0, 0, colornames.Red) - r.expect(8, 8, colornames.Red) - - // TODO: this currently seems to do srgb scaling - // instead of linear rgb scaling, - r.expect(64-4, 0, color.RGBA{R: 197, G: 87, B: 0, A: 255}) - r.expect(64+4, 0, color.RGBA{R: 175, G: 98, B: 0, A: 255}) - - r.expect(127, 0, colornames.Green) - r.expect(127-8, 8, colornames.Green) - }) -} - -func TestImageRGBA_ScaleNearest(t *testing.T) { - run(t, func(o *op.Ops) { - w := newWindow(t, 128, 128) - op.Affine(f32.AffineId().Scale(f32.Point{}, f32.Pt(64, 64))).Add(o) - - im := image.NewRGBA(image.Rect(0, 0, 2, 2)) - im.Set(0, 0, colornames.Red) - im.Set(1, 0, colornames.Green) - im.Set(0, 1, colornames.White) - im.Set(1, 1, colornames.Black) - - op := paint.NewImageOp(im) - op.Filter = paint.FilterNearest - op.Add(o) - - paint.PaintOp{}.Add(o) - - if err := w.Frame(o); err != nil { - t.Error(err) - } - }, func(r result) { - r.expect(0, 0, colornames.Red) - r.expect(8, 8, colornames.Red) - - r.expect(64-4, 0, colornames.Red) - r.expect(64+4, 0, colornames.Green) - - r.expect(127, 0, colornames.Green) - r.expect(127-8, 8, colornames.Green) - }) -} - -func TestGapsInPath(t *testing.T) { - ops := new(op.Ops) - var p clip.Path - p.Begin(ops) - // Unclosed square 1 - p.MoveTo(f32.Point{X: 10}) - p.LineTo(f32.Point{X: 40}) - p.LineTo(f32.Point{X: 40, Y: 30}) - p.LineTo(f32.Point{X: 10, Y: 30}) - - // Unclosed square 2 - p.MoveTo(f32.Point{X: 50}) - p.LineTo(f32.Point{X: 80}) - p.LineTo(f32.Point{X: 80, Y: 30}) - p.LineTo(f32.Point{X: 50, Y: 30}) - - spec := p.End() - - t.Run("Stroke", func(t *testing.T) { - run(t, - func(ops *op.Ops) { - stack := clip.Stroke{ - Path: spec, - Width: 2, - }.Op().Push(ops) - paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops) - paint.PaintOp{}.Add(ops) - stack.Pop() - }, - func(r result) { - r.expect(10, 20, color.RGBA{}) - r.expect(50, 20, color.RGBA{}) - }, - ) - }) - - t.Run("Outline", func(t *testing.T) { - run(t, - func(ops *op.Ops) { - stack := clip.Outline{Path: spec}.Op().Push(ops) - paint.ColorOp{Color: color.NRGBA{R: 255, A: 255}}.Add(ops) - paint.PaintOp{}.Add(ops) - stack.Pop() - }, - func(r result) { - r.expect(10, 20, colornames.Red) - r.expect(20, 20, colornames.Red) - r.expect(50, 20, colornames.Red) - r.expect(60, 20, colornames.Red) - }, - ) - }) -} - -func TestOpacity(t *testing.T) { - run(t, func(ops *op.Ops) { - opc1 := paint.PushOpacity(ops, .3) - // Fill screen to exercise the glClear optimization. - paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op()) - opc2 := paint.PushOpacity(ops, .6) - paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op()) - opc2.Pop() - opc1.Pop() - opc3 := paint.PushOpacity(ops, .6) - paint.FillShape(ops, color.NRGBA{B: 255, A: 255}, clip.Ellipse(image.Rectangle{Min: image.Pt(20+20, 10), Max: image.Pt(50+64, 128)}).Op(ops)) - opc3.Pop() - }, nil) -} - -// lerp calculates linear interpolation with color b and p. -func lerp(a, b f32color.RGBA, p float32) f32color.RGBA { - return f32color.RGBA{ - R: a.R*(1-p) + b.R*p, - G: a.G*(1-p) + b.G*p, - B: a.B*(1-p) + b.B*p, - A: a.A*(1-p) + b.A*p, - } -} diff --git a/gio/gpu/internal/rendertest/transform_test.go b/gio/gpu/internal/rendertest/transform_test.go deleted file mode 100644 index 31f58a9..0000000 --- a/gio/gpu/internal/rendertest/transform_test.go +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package rendertest - -import ( - "image" - "math" - "testing" - - "golang.org/x/image/colornames" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -func TestPaintOffset(t *testing.T) { - run(t, func(o *op.Ops) { - defer op.Offset(image.Pt(10, 20)).Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 50, 50)).Op()) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(59, 30, colornames.Red) - r.expect(60, 30, transparent) - r.expect(10, 70, transparent) - }) -} - -func TestPaintRotate(t *testing.T) { - run(t, func(o *op.Ops) { - a := f32.AffineId().Rotate(f32.Pt(40, 40), -math.Pi/8) - defer op.Affine(a).Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(20, 20, 60, 60)).Op()) - }, func(r result) { - r.expect(40, 40, colornames.Red) - r.expect(50, 19, colornames.Red) - r.expect(59, 19, transparent) - r.expect(21, 21, transparent) - }) -} - -func TestPaintShear(t *testing.T) { - run(t, func(o *op.Ops) { - a := f32.AffineId().Shear(f32.Point{}, math.Pi/4, 0) - defer op.Affine(a).Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 40, 40)).Op()) - }, func(r result) { - r.expect(10, 30, transparent) - }) -} - -func TestClipPaintOffset(t *testing.T) { - run(t, func(o *op.Ops) { - defer clip.RRect{Rect: image.Rect(10, 10, 30, 30)}.Push(o).Pop() - defer op.Offset(image.Pt(20, 20)).Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op()) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(19, 19, transparent) - r.expect(20, 20, colornames.Red) - r.expect(30, 30, transparent) - }) -} - -func TestClipOffset(t *testing.T) { - run(t, func(o *op.Ops) { - defer op.Offset(image.Pt(20, 20)).Push(o).Pop() - defer clip.RRect{Rect: image.Rect(10, 10, 30, 30)}.Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 100, 100)).Op()) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(29, 29, transparent) - r.expect(30, 30, colornames.Red) - r.expect(49, 49, colornames.Red) - r.expect(50, 50, transparent) - }) -} - -func TestClipScale(t *testing.T) { - run(t, func(o *op.Ops) { - a := f32.AffineId().Scale(f32.Point{}, f32.Pt(2, 2)).Offset(f32.Pt(10, 10)) - defer op.Affine(a).Push(o).Pop() - defer clip.RRect{Rect: image.Rect(10, 10, 20, 20)}.Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 1000, 1000)).Op()) - }, func(r result) { - r.expect(19+10, 19+10, transparent) - r.expect(20+10, 20+10, colornames.Red) - r.expect(39+10, 39+10, colornames.Red) - r.expect(40+10, 40+10, transparent) - }) -} - -func TestClipRotate(t *testing.T) { - run(t, func(o *op.Ops) { - defer op.Affine(f32.AffineId().Rotate(f32.Pt(40, 40), -math.Pi/4)).Push(o).Pop() - defer clip.RRect{Rect: image.Rect(30, 30, 50, 50)}.Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(0, 40, 100, 100)).Op()) - }, func(r result) { - r.expect(39, 39, transparent) - r.expect(41, 41, colornames.Red) - r.expect(50, 50, transparent) - }) -} - -func TestOffsetTexture(t *testing.T) { - run(t, func(o *op.Ops) { - defer op.Offset(image.Pt(15, 15)).Push(o).Pop() - squares.Add(o) - defer scale(50.0/512, 50.0/512).Push(o).Pop() - paint.PaintOp{}.Add(o) - }, func(r result) { - r.expect(14, 20, transparent) - r.expect(66, 20, transparent) - r.expect(16, 64, colornames.Green) - r.expect(64, 16, colornames.Green) - }) -} - -func TestOffsetScaleTexture(t *testing.T) { - run(t, func(o *op.Ops) { - defer op.Offset(image.Pt(15, 15)).Push(o).Pop() - squares.Add(o) - defer op.Affine(f32.AffineId().Scale(f32.Point{}, f32.Pt(2, 1))).Push(o).Pop() - defer scale(50.0/512, 50.0/512).Push(o).Pop() - paint.PaintOp{}.Add(o) - }, func(r result) { - r.expect(114, 64, colornames.Blue) - r.expect(116, 64, transparent) - }) -} - -func TestRotateTexture(t *testing.T) { - run(t, func(o *op.Ops) { - squares.Add(o) - a := f32.AffineId().Offset(f32.Pt(30, 30)).Rotate(f32.Pt(40, 40), math.Pi/4) - defer op.Affine(a).Push(o).Pop() - defer scale(20.0/512, 20.0/512).Push(o).Pop() - paint.PaintOp{}.Add(o) - }, func(r result) { - r.expect(40, 40-12, colornames.Blue) - r.expect(40+12, 40, colornames.Green) - }) -} - -func TestRotateClipTexture(t *testing.T) { - run(t, func(o *op.Ops) { - squares.Add(o) - a := f32.AffineId().Rotate(f32.Pt(40, 40), math.Pi/8) - defer op.Affine(a).Push(o).Pop() - defer clip.RRect{Rect: image.Rect(30, 30, 50, 50)}.Push(o).Pop() - defer op.Affine(f32.AffineId().Offset(f32.Pt(10, 10))).Push(o).Pop() - defer scale(60.0/512, 60.0/512).Push(o).Pop() - paint.PaintOp{}.Add(o) - }, func(r result) { - r.expect(0, 0, transparent) - r.expect(37, 39, colornames.Green) - r.expect(36, 39, colornames.Green) - r.expect(35, 39, colornames.Green) - r.expect(34, 39, colornames.Green) - r.expect(33, 39, colornames.Green) - }) -} - -func TestComplicatedTransform(t *testing.T) { - run(t, func(o *op.Ops) { - squares.Add(o) - - defer clip.RRect{Rect: image.Rect(0, 0, 100, 100), SE: 50, SW: 50, NW: 50, NE: 50}.Push(o).Pop() - - a := f32.AffineId().Shear(f32.Point{}, math.Pi/4, 0) - defer op.Affine(a).Push(o).Pop() - defer clip.RRect{Rect: image.Rect(0, 0, 50, 40)}.Push(o).Pop() - - defer scale(50.0/512, 50.0/512).Push(o).Pop() - paint.PaintOp{}.Add(o) - }, func(r result) { - r.expect(20, 5, transparent) - }) -} - -func TestTransformOrder(t *testing.T) { - // check the ordering of operations bot in affine and in gpu stack. - run(t, func(o *op.Ops) { - a := f32.AffineId().Offset(f32.Pt(64, 64)) - defer op.Affine(a).Push(o).Pop() - - b := f32.AffineId().Scale(f32.Point{}, f32.Pt(8, 8)) - defer op.Affine(b).Push(o).Pop() - - c := f32.AffineId().Offset(f32.Pt(-10, -10)).Scale(f32.Point{}, f32.Pt(0.5, 0.5)) - defer op.Affine(c).Push(o).Pop() - paint.FillShape(o, red, clip.Rect(image.Rect(0, 0, 20, 20)).Op()) - }, func(r result) { - // centered and with radius 40 - r.expect(64-41, 64, transparent) - r.expect(64-39, 64, colornames.Red) - r.expect(64+39, 64, colornames.Red) - r.expect(64+41, 64, transparent) - }) -} diff --git a/gio/gpu/internal/rendertest/util_test.go b/gio/gpu/internal/rendertest/util_test.go deleted file mode 100644 index f5bf35b..0000000 --- a/gio/gpu/internal/rendertest/util_test.go +++ /dev/null @@ -1,306 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package rendertest - -import ( - "bytes" - "flag" - "fmt" - "image" - "image/color" - "image/draw" - "image/png" - "os" - "path/filepath" - "strconv" - "testing" - - "golang.org/x/image/colornames" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/gpu/headless" - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -var ( - dumpImages = flag.Bool("saveimages", false, "save test images") - squares paint.ImageOp - smallSquares paint.ImageOp -) - -var ( - red = f32color.RGBAToNRGBA(colornames.Red) - green = f32color.RGBAToNRGBA(colornames.Green) - blue = f32color.RGBAToNRGBA(colornames.Blue) - magenta = f32color.RGBAToNRGBA(colornames.Magenta) - black = f32color.RGBAToNRGBA(colornames.Black) - white = f32color.RGBAToNRGBA(colornames.White) - transparent = color.RGBA{} -) - -func init() { - squares = buildSquares(512) - smallSquares = buildSquares(50) -} - -func buildSquares(size int) paint.ImageOp { - sub := size / 4 - im := image.NewNRGBA(image.Rect(0, 0, size, size)) - c1, c2 := image.NewUniform(colornames.Green), image.NewUniform(colornames.Blue) - for r := range 4 { - for c := range 4 { - c1, c2 = c2, c1 - draw.Draw(im, image.Rect(r*sub, c*sub, r*sub+sub, c*sub+sub), c1, image.Point{}, draw.Over) - } - c1, c2 = c2, c1 - } - return paint.NewImageOp(im) -} - -func drawImage(t *testing.T, size int, ops *op.Ops, draw func(o *op.Ops)) (*image.RGBA, error) { - sz := image.Point{X: size, Y: size} - w := newWindow(t, sz.X, sz.Y) - defer w.Release() - draw(ops) - if err := w.Frame(ops); err != nil { - return nil, err - } - img := image.NewRGBA(image.Rectangle{Max: sz}) - err := w.Screenshot(img) - return img, err -} - -func run(t *testing.T, f func(o *op.Ops), c func(r result)) { - // Draw a few times and check that it is correct each time, to - // ensure any caching effects still generate the correct images. - var img *image.RGBA - var err error - ops := new(op.Ops) - for i := range 3 { - ops.Reset() - img, err = drawImage(t, 128, ops, f) - if err != nil { - t.Error("error rendering:", err) - return - } - // Check for a reference image and make sure it is identical. - if !verifyRef(t, img, 0) { - name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i) - saveImage(t, name, img) - } - if c != nil { - c(result{t: t, img: img}) - } - } -} - -func frame(f func(o *op.Ops), c func(r result)) frameT { - return frameT{f: f, c: c} -} - -type frameT struct { - f func(o *op.Ops) - c func(r result) -} - -// multiRun is used to run test cases over multiple frames, typically -// to test caching interactions. -func multiRun(t *testing.T, frames ...frameT) { - // draw a few times and check that it is correct each time, to - // ensure any caching effects still generate the correct images. - var err error - sz := image.Point{X: 128, Y: 128} - w := newWindow(t, sz.X, sz.Y) - defer w.Release() - ops := new(op.Ops) - for i := range frames { - ops.Reset() - frames[i].f(ops) - if err := w.Frame(ops); err != nil { - t.Errorf("rendering failed: %v", err) - continue - } - img := image.NewRGBA(image.Rectangle{Max: sz}) - err = w.Screenshot(img) - if err != nil { - t.Errorf("screenshot failed: %v", err) - continue - } - // Check for a reference image and make sure they are identical. - ok := verifyRef(t, img, i) - if frames[i].c != nil { - frames[i].c(result{t: t, img: img}) - } - if !ok { - name := t.Name() + ".png" - if i != 0 { - name = t.Name() + "_" + strconv.Itoa(i) + ".png" - } - saveImage(t, name, img) - } - } -} - -func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) { - // ensure identical to ref data - var path string - if frame == 0 { - path = t.Name() - } else { - path = t.Name() + "_" + strconv.Itoa(frame) - } - path = filepath.Join("refs", path+".png") - if *dumpImages { - if err := os.MkdirAll(filepath.Dir(path), 0o766); err != nil { - if !os.IsExist(err) { - t.Error(err) - return - } - } - saveImage(t, path, img) - return true - } - b, err := os.ReadFile(path) - if err != nil { - t.Error("could not open ref:", err) - return - } - r, err := png.Decode(bytes.NewReader(b)) - if err != nil { - t.Error("could not decode ref:", err) - return - } - if img.Bounds() != r.Bounds() { - t.Errorf("reference image is %v, expected %v", r.Bounds(), img.Bounds()) - return false - } - var ref *image.RGBA - switch r := r.(type) { - case *image.RGBA: - ref = r - case *image.NRGBA: - ref = image.NewRGBA(r.Bounds()) - bnd := r.Bounds() - for x := bnd.Min.X; x < bnd.Max.X; x++ { - for y := bnd.Min.Y; y < bnd.Max.Y; y++ { - ref.SetRGBA(x, y, f32color.NRGBAToRGBA(r.NRGBAAt(x, y))) - } - } - default: - t.Fatalf("reference image is a %T, expected *image.NRGBA or *image.RGBA", r) - } - bnd := img.Bounds() - for x := bnd.Min.X; x < bnd.Max.X; x++ { - for y := bnd.Min.Y; y < bnd.Max.Y; y++ { - exp := ref.RGBAAt(x, y) - got := img.RGBAAt(x, y) - if !colorsClose(exp, got) || !alphaClose(exp, got) { - t.Error("not equal to ref at", x, y, " ", got, exp) - return false - } - } - } - return true -} - -func colorsClose(c1, c2 color.RGBA) bool { - const delta = 0.01 // magic value obtained from experimentation. - return yiqEqApprox(c1, c2, delta) -} - -func alphaClose(c1, c2 color.RGBA) bool { - d := int(c1.A) - int(c2.A) - return d > -8 && d < 8 -} - -// yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space, -// as described in: -// -// Measuring perceived color difference using YIQ NTSC -// transmission color space in mobile applications. -// Yuriy Kotsarenko, Fernando Ramos. -// -// An electronic version is available at: -// -// - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf -func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool { - const max = 35215.0 // difference between 2 maximally different pixels. - - var ( - r1 = float64(c1.R) - g1 = float64(c1.G) - b1 = float64(c1.B) - - r2 = float64(c2.R) - g2 = float64(c2.G) - b2 = float64(c2.B) - - y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223 - i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189 - q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694 - - y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223 - i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189 - q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694 - - y = y1 - y2 - i = i1 - i2 - q = q1 - q2 - - diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q - ) - return diff <= max*d2 -} - -func (r result) expect(x, y int, col color.RGBA) { - r.t.Helper() - if r.img == nil { - return - } - c := r.img.RGBAAt(x, y) - if !colorsClose(c, col) { - r.t.Error("expected ", col, " at ", "(", x, ",", y, ") but got ", c) - } -} - -type result struct { - t *testing.T - img *image.RGBA -} - -func saveImage(t testing.TB, file string, img *image.RGBA) { - if !*dumpImages { - return - } - // Only NRGBA images are losslessly encoded by png.Encode. - nrgba := image.NewNRGBA(img.Bounds()) - bnd := img.Bounds() - for x := bnd.Min.X; x < bnd.Max.X; x++ { - for y := bnd.Min.Y; y < bnd.Max.Y; y++ { - nrgba.SetNRGBA(x, y, f32color.RGBAToNRGBA(img.RGBAAt(x, y))) - } - } - var buf bytes.Buffer - if err := png.Encode(&buf, nrgba); err != nil { - t.Error(err) - return - } - if err := os.WriteFile(file, buf.Bytes(), 0o666); err != nil { - t.Error(err) - return - } -} - -func newWindow(t testing.TB, width, height int) *headless.Window { - w, err := headless.NewWindow(width, height) - if err != nil { - t.Skipf("failed to create headless window, skipping: %v", err) - } - return w -} - -func scale(sx, sy float32) op.TransformOp { - return op.Affine(f32.AffineId().Scale(f32.Point{}, f32.Pt(sx, sy))) -} diff --git a/gio/gpu/internal/vulkan/vulkan.go b/gio/gpu/internal/vulkan/vulkan.go deleted file mode 100644 index fd6cbab..0000000 --- a/gio/gpu/internal/vulkan/vulkan.go +++ /dev/null @@ -1,1163 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build (linux || freebsd) && !novulkan -// +build linux freebsd -// +build !novulkan - -package vulkan - -import ( - "errors" - "fmt" - "image" - "math/bits" - - "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" - "github.com/p9c/p9/pkg/gel/gio/internal/vk" - "github.com/p9c/p9/pkg/gel/gio/shader" -) - -type Backend struct { - physDev vk.PhysicalDevice - dev vk.Device - queue vk.Queue - cmdPool struct { - current vk.CommandBuffer - pool vk.CommandPool - used int - buffers []vk.CommandBuffer - } - outFormat vk.Format - staging struct { - buf *Buffer - mem []byte - size int - cap int - } - defers []func(d vk.Device) - frameSig vk.Semaphore - frameFence vk.Fence - waitSems []vk.Semaphore - waitStages []vk.PipelineStageFlags - sigSems []vk.Semaphore - fence vk.Fence - - allPipes []*Pipeline - - pipe *Pipeline - - passes map[passKey]vk.RenderPass - - // bindings and offset are temporary storage for BindVertexBuffer. - bindings []vk.Buffer - offsets []vk.DeviceSize - - desc struct { - dirty bool - texBinds [texUnits]*Texture - bufBinds [storageUnits]*Buffer - } - - caps driver.Features -} - -type passKey struct { - fmt vk.Format - loadAct vk.AttachmentLoadOp - initLayout vk.ImageLayout - finalLayout vk.ImageLayout -} - -type Texture struct { - backend *Backend - img vk.Image - mem vk.DeviceMemory - view vk.ImageView - sampler vk.Sampler - fbo vk.Framebuffer - format vk.Format - mipmaps int - layout vk.ImageLayout - passLayout vk.ImageLayout - width int - height int - acquire vk.Semaphore - foreign bool - - scope struct { - stage vk.PipelineStageFlags - access vk.AccessFlags - } -} - -type Shader struct { - dev vk.Device - module vk.ShaderModule - pushRange vk.PushConstantRange - src shader.Sources -} - -type Pipeline struct { - backend *Backend - pipe vk.Pipeline - pushRanges []vk.PushConstantRange - ninputs int - desc *descPool -} - -type descPool struct { - layout vk.PipelineLayout - descLayout vk.DescriptorSetLayout - pool vk.DescriptorPool - sets []vk.DescriptorSet - size int - texBinds []int - imgBinds []int - bufBinds []int -} - -type Buffer struct { - backend *Backend - buf vk.Buffer - store []byte - mem vk.DeviceMemory - usage vk.BufferUsageFlags - - scope struct { - stage vk.PipelineStageFlags - access vk.AccessFlags - } -} - -const ( - texUnits = 4 - storageUnits = 4 -) - -func init() { - driver.NewVulkanDevice = newVulkanDevice -} - -func newVulkanDevice(api driver.Vulkan) (driver.Device, error) { - b := &Backend{ - physDev: vk.PhysicalDevice(api.PhysDevice), - dev: vk.Device(api.Device), - outFormat: vk.Format(api.Format), - caps: driver.FeatureCompute, - passes: make(map[passKey]vk.RenderPass), - } - b.queue = vk.GetDeviceQueue(b.dev, api.QueueFamily, api.QueueIndex) - cmdPool, err := vk.CreateCommandPool(b.dev, api.QueueFamily) - if err != nil { - return nil, err - } - b.cmdPool.pool = cmdPool - props := vk.GetPhysicalDeviceFormatProperties(b.physDev, vk.FORMAT_R16_SFLOAT) - reqs := vk.FORMAT_FEATURE_COLOR_ATTACHMENT_BIT | vk.FORMAT_FEATURE_SAMPLED_IMAGE_BIT - if props&reqs == reqs { - b.caps |= driver.FeatureFloatRenderTargets - } - reqs = vk.FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT | vk.FORMAT_FEATURE_SAMPLED_IMAGE_BIT | vk.FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT - props = vk.GetPhysicalDeviceFormatProperties(b.physDev, vk.FORMAT_R8G8B8A8_SRGB) - if props&reqs == reqs { - b.caps |= driver.FeatureSRGB - } - fence, err := vk.CreateFence(b.dev, 0) - if err != nil { - return nil, mapErr(err) - } - b.fence = fence - return b, nil -} - -func (b *Backend) BeginFrame(target driver.RenderTarget, clear bool, viewport image.Point) driver.Texture { - b.staging.size = 0 - b.cmdPool.used = 0 - b.runDefers() - b.resetPipes() - - if target == nil { - return nil - } - switch t := target.(type) { - case driver.VulkanRenderTarget: - layout := vk.IMAGE_LAYOUT_UNDEFINED - if !clear { - layout = vk.IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL - } - b.frameSig = vk.Semaphore(t.SignalSem) - b.frameFence = vk.Fence(t.Fence) - tex := &Texture{ - img: vk.Image(t.Image), - fbo: vk.Framebuffer(t.Framebuffer), - width: viewport.X, - height: viewport.Y, - layout: layout, - passLayout: vk.IMAGE_LAYOUT_PRESENT_SRC_KHR, - format: b.outFormat, - acquire: vk.Semaphore(t.WaitSem), - foreign: true, - } - return tex - case *Texture: - return t - default: - panic(fmt.Sprintf("vulkan: unsupported render target type: %T", t)) - } -} - -func (b *Backend) deferFunc(f func(d vk.Device)) { - b.defers = append(b.defers, f) -} - -func (b *Backend) runDefers() { - for _, f := range b.defers { - f(b.dev) - } - b.defers = b.defers[:0] -} - -func (b *Backend) resetPipes() { - for i := len(b.allPipes) - 1; i >= 0; i-- { - p := b.allPipes[i] - if p.pipe == 0 { - // Released pipeline. - b.allPipes = append(b.allPipes[:i], b.allPipes[:i+1]...) - continue - } - if p.desc.size > 0 { - p.desc.size = 0 - } - } -} - -func (b *Backend) EndFrame() { - if b.frameSig != 0 { - b.sigSems = append(b.sigSems, b.frameSig) - b.frameSig = 0 - } - fence := b.frameFence - if fence == 0 { - // We're internally synchronized. - fence = b.fence - } - b.submitCmdBuf(fence) - if b.frameFence == 0 { - vk.WaitForFences(b.dev, fence) - vk.ResetFences(b.dev, fence) - } -} - -func (b *Backend) Caps() driver.Caps { - return driver.Caps{ - MaxTextureSize: 4096, - Features: b.caps, - } -} - -func (b *Backend) NewTimer() driver.Timer { - panic("timers not supported") -} - -func (b *Backend) IsTimeContinuous() bool { - panic("timers not supported") -} - -func (b *Backend) Release() { - vk.DeviceWaitIdle(b.dev) - if buf := b.staging.buf; buf != nil { - vk.UnmapMemory(b.dev, b.staging.buf.mem) - buf.Release() - } - b.runDefers() - for _, rp := range b.passes { - vk.DestroyRenderPass(b.dev, rp) - } - vk.DestroyFence(b.dev, b.fence) - vk.FreeCommandBuffers(b.dev, b.cmdPool.pool, b.cmdPool.buffers...) - vk.DestroyCommandPool(b.dev, b.cmdPool.pool) - *b = Backend{} -} - -func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, minFilter, magFilter driver.TextureFilter, bindings driver.BufferBinding) (driver.Texture, error) { - vkfmt := formatFor(format) - usage := vk.IMAGE_USAGE_TRANSFER_DST_BIT | vk.IMAGE_USAGE_TRANSFER_SRC_BIT - passLayout := vk.IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL - if bindings&driver.BufferBindingTexture != 0 { - usage |= vk.IMAGE_USAGE_SAMPLED_BIT - passLayout = vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL - } - if bindings&driver.BufferBindingFramebuffer != 0 { - usage |= vk.IMAGE_USAGE_COLOR_ATTACHMENT_BIT - } - if bindings&(driver.BufferBindingShaderStorageRead|driver.BufferBindingShaderStorageWrite) != 0 { - usage |= vk.IMAGE_USAGE_STORAGE_BIT - } - filterFor := func(f driver.TextureFilter) vk.Filter { - switch f { - case driver.FilterLinear, driver.FilterLinearMipmapLinear: - return vk.FILTER_LINEAR - case driver.FilterNearest: - return vk.FILTER_NEAREST - } - panic("unknown filter") - } - mipmapMode := vk.SAMPLER_MIPMAP_MODE_NEAREST - mipmap := minFilter == driver.FilterLinearMipmapLinear - nmipmaps := 1 - if mipmap { - mipmapMode = vk.SAMPLER_MIPMAP_MODE_LINEAR - dim := width - if height > dim { - dim = height - } - log2 := 32 - bits.LeadingZeros32(uint32(dim)) - 1 - nmipmaps = log2 + 1 - } - sampler, err := vk.CreateSampler(b.dev, filterFor(minFilter), filterFor(magFilter), mipmapMode) - if err != nil { - return nil, mapErr(err) - } - img, mem, err := vk.CreateImage(b.physDev, b.dev, vkfmt, width, height, nmipmaps, usage) - if err != nil { - vk.DestroySampler(b.dev, sampler) - return nil, mapErr(err) - } - view, err := vk.CreateImageView(b.dev, img, vkfmt) - if err != nil { - vk.DestroySampler(b.dev, sampler) - vk.DestroyImage(b.dev, img) - vk.FreeMemory(b.dev, mem) - return nil, mapErr(err) - } - t := &Texture{backend: b, img: img, mem: mem, view: view, sampler: sampler, layout: vk.IMAGE_LAYOUT_UNDEFINED, passLayout: passLayout, width: width, height: height, format: vkfmt, mipmaps: nmipmaps} - if bindings&driver.BufferBindingFramebuffer != 0 { - pass, err := vk.CreateRenderPass(b.dev, vkfmt, vk.ATTACHMENT_LOAD_OP_DONT_CARE, - vk.IMAGE_LAYOUT_UNDEFINED, vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, nil) - if err != nil { - return nil, mapErr(err) - } - defer vk.DestroyRenderPass(b.dev, pass) - fbo, err := vk.CreateFramebuffer(b.dev, pass, view, width, height) - if err != nil { - return nil, mapErr(err) - } - t.fbo = fbo - } - return t, nil -} - -func (b *Backend) NewBuffer(bindings driver.BufferBinding, size int) (driver.Buffer, error) { - if bindings&driver.BufferBindingUniforms != 0 { - // Implement uniform buffers as inline push constants. - return &Buffer{store: make([]byte, size)}, nil - } - usage := vk.BUFFER_USAGE_TRANSFER_DST_BIT | vk.BUFFER_USAGE_TRANSFER_SRC_BIT - if bindings&driver.BufferBindingIndices != 0 { - usage |= vk.BUFFER_USAGE_INDEX_BUFFER_BIT - } - if bindings&(driver.BufferBindingShaderStorageRead|driver.BufferBindingShaderStorageWrite) != 0 { - usage |= vk.BUFFER_USAGE_STORAGE_BUFFER_BIT - } - if bindings&driver.BufferBindingVertices != 0 { - usage |= vk.BUFFER_USAGE_VERTEX_BUFFER_BIT - } - buf, err := b.newBuffer(size, usage, vk.MEMORY_PROPERTY_DEVICE_LOCAL_BIT) - return buf, mapErr(err) -} - -func (b *Backend) newBuffer(size int, usage vk.BufferUsageFlags, props vk.MemoryPropertyFlags) (*Buffer, error) { - buf, mem, err := vk.CreateBuffer(b.physDev, b.dev, size, usage, props) - return &Buffer{backend: b, buf: buf, mem: mem, usage: usage}, err -} - -func (b *Backend) NewImmutableBuffer(typ driver.BufferBinding, data []byte) (driver.Buffer, error) { - buf, err := b.NewBuffer(typ, len(data)) - if err != nil { - return nil, err - } - buf.Upload(data) - return buf, nil -} - -func (b *Backend) NewVertexShader(src shader.Sources) (driver.VertexShader, error) { - sh, err := b.newShader(src, vk.SHADER_STAGE_VERTEX_BIT) - return sh, mapErr(err) -} - -func (b *Backend) NewFragmentShader(src shader.Sources) (driver.FragmentShader, error) { - sh, err := b.newShader(src, vk.SHADER_STAGE_FRAGMENT_BIT) - return sh, mapErr(err) -} - -func (b *Backend) NewPipeline(desc driver.PipelineDesc) (driver.Pipeline, error) { - vs := desc.VertexShader.(*Shader) - fs := desc.FragmentShader.(*Shader) - var ranges []vk.PushConstantRange - if r := vs.pushRange; r != (vk.PushConstantRange{}) { - ranges = append(ranges, r) - } - if r := fs.pushRange; r != (vk.PushConstantRange{}) { - ranges = append(ranges, r) - } - descPool, err := createPipelineLayout(b.dev, fs.src, ranges) - if err != nil { - return nil, mapErr(err) - } - blend := desc.BlendDesc - factorFor := func(f driver.BlendFactor) vk.BlendFactor { - switch f { - case driver.BlendFactorZero: - return vk.BLEND_FACTOR_ZERO - case driver.BlendFactorOne: - return vk.BLEND_FACTOR_ONE - case driver.BlendFactorOneMinusSrcAlpha: - return vk.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA - case driver.BlendFactorDstColor: - return vk.BLEND_FACTOR_DST_COLOR - default: - panic("unknown blend factor") - } - } - var top vk.PrimitiveTopology - switch desc.Topology { - case driver.TopologyTriangles: - top = vk.PRIMITIVE_TOPOLOGY_TRIANGLE_LIST - case driver.TopologyTriangleStrip: - top = vk.PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP - default: - panic("unknown topology") - } - var binds []vk.VertexInputBindingDescription - var attrs []vk.VertexInputAttributeDescription - inputs := desc.VertexLayout.Inputs - for i, inp := range inputs { - binds = append(binds, vk.VertexInputBindingDescription{ - Binding: i, - Stride: desc.VertexLayout.Stride, - }) - attrs = append(attrs, vk.VertexInputAttributeDescription{ - Binding: i, - Location: vs.src.Inputs[i].Location, - Format: vertFormatFor(vs.src.Inputs[i]), - Offset: inp.Offset, - }) - } - fmt := b.outFormat - if f := desc.PixelFormat; f != driver.TextureFormatOutput { - fmt = formatFor(f) - } - pass, err := vk.CreateRenderPass(b.dev, fmt, vk.ATTACHMENT_LOAD_OP_DONT_CARE, - vk.IMAGE_LAYOUT_UNDEFINED, vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, nil) - if err != nil { - return nil, mapErr(err) - } - defer vk.DestroyRenderPass(b.dev, pass) - pipe, err := vk.CreateGraphicsPipeline(b.dev, pass, vs.module, fs.module, blend.Enable, factorFor(blend.SrcFactor), factorFor(blend.DstFactor), top, binds, attrs, descPool.layout) - if err != nil { - descPool.release(b.dev) - return nil, mapErr(err) - } - p := &Pipeline{backend: b, pipe: pipe, desc: descPool, pushRanges: ranges, ninputs: len(inputs)} - b.allPipes = append(b.allPipes, p) - return p, nil -} - -func (b *Backend) NewComputeProgram(src shader.Sources) (driver.Program, error) { - sh, err := b.newShader(src, vk.SHADER_STAGE_COMPUTE_BIT) - if err != nil { - return nil, mapErr(err) - } - defer sh.Release() - descPool, err := createPipelineLayout(b.dev, src, nil) - if err != nil { - return nil, mapErr(err) - } - pipe, err := vk.CreateComputePipeline(b.dev, sh.module, descPool.layout) - if err != nil { - descPool.release(b.dev) - return nil, mapErr(err) - } - return &Pipeline{backend: b, pipe: pipe, desc: descPool}, nil -} - -func vertFormatFor(f shader.InputLocation) vk.Format { - t := f.Type - s := f.Size - switch { - case t == shader.DataTypeFloat && s == 1: - return vk.FORMAT_R32_SFLOAT - case t == shader.DataTypeFloat && s == 2: - return vk.FORMAT_R32G32_SFLOAT - case t == shader.DataTypeFloat && s == 3: - return vk.FORMAT_R32G32B32_SFLOAT - case t == shader.DataTypeFloat && s == 4: - return vk.FORMAT_R32G32B32A32_SFLOAT - default: - panic("unsupported data type") - } -} - -func createPipelineLayout(d vk.Device, src shader.Sources, ranges []vk.PushConstantRange) (*descPool, error) { - var ( - descLayouts []vk.DescriptorSetLayout - descLayout vk.DescriptorSetLayout - ) - texBinds := make([]int, len(src.Textures)) - imgBinds := make([]int, len(src.Images)) - bufBinds := make([]int, len(src.StorageBuffers)) - var descBinds []vk.DescriptorSetLayoutBinding - for i, t := range src.Textures { - descBinds = append(descBinds, vk.DescriptorSetLayoutBinding{ - Binding: t.Binding, - StageFlags: vk.SHADER_STAGE_FRAGMENT_BIT, - DescriptorType: vk.DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, - }) - texBinds[i] = t.Binding - } - for i, img := range src.Images { - descBinds = append(descBinds, vk.DescriptorSetLayoutBinding{ - Binding: img.Binding, - StageFlags: vk.SHADER_STAGE_COMPUTE_BIT, - DescriptorType: vk.DESCRIPTOR_TYPE_STORAGE_IMAGE, - }) - imgBinds[i] = img.Binding - } - for i, buf := range src.StorageBuffers { - descBinds = append(descBinds, vk.DescriptorSetLayoutBinding{ - Binding: buf.Binding, - StageFlags: vk.SHADER_STAGE_COMPUTE_BIT, - DescriptorType: vk.DESCRIPTOR_TYPE_STORAGE_BUFFER, - }) - bufBinds[i] = buf.Binding - } - if len(descBinds) > 0 { - var err error - descLayout, err = vk.CreateDescriptorSetLayout(d, descBinds) - if err != nil { - return nil, err - } - descLayouts = append(descLayouts, descLayout) - } - layout, err := vk.CreatePipelineLayout(d, ranges, descLayouts) - if err != nil { - if descLayout != 0 { - vk.DestroyDescriptorSetLayout(d, descLayout) - } - return nil, err - } - descPool := &descPool{ - texBinds: texBinds, - bufBinds: bufBinds, - imgBinds: imgBinds, - layout: layout, - descLayout: descLayout, - } - return descPool, nil -} - -func (b *Backend) newShader(src shader.Sources, stage vk.ShaderStageFlags) (*Shader, error) { - mod, err := vk.CreateShaderModule(b.dev, src.SPIRV) - if err != nil { - return nil, err - } - - sh := &Shader{dev: b.dev, module: mod, src: src} - if locs := src.Uniforms.Locations; len(locs) > 0 { - pushOffset := 0x7fffffff - for _, l := range locs { - if l.Offset < pushOffset { - pushOffset = l.Offset - } - } - sh.pushRange = vk.BuildPushConstantRange(stage, pushOffset, src.Uniforms.Size) - } - return sh, nil -} - -func (b *Backend) CopyTexture(dstTex driver.Texture, dorig image.Point, srcFBO driver.Texture, srect image.Rectangle) { - dst := dstTex.(*Texture) - src := srcFBO.(*Texture) - cmdBuf := b.ensureCmdBuf() - op := vk.BuildImageCopy(srect.Min.X, srect.Min.Y, dorig.X, dorig.Y, srect.Dx(), srect.Dy()) - src.imageBarrier(cmdBuf, - vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - vk.PIPELINE_STAGE_TRANSFER_BIT, - vk.ACCESS_TRANSFER_READ_BIT, - ) - dst.imageBarrier(cmdBuf, - vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - vk.PIPELINE_STAGE_TRANSFER_BIT, - vk.ACCESS_TRANSFER_WRITE_BIT, - ) - vk.CmdCopyImage(cmdBuf, src.img, src.layout, dst.img, dst.layout, []vk.ImageCopy{op}) -} - -func (b *Backend) Viewport(x, y, width, height int) { - cmdBuf := b.currentCmdBuf() - vp := vk.BuildViewport(float32(x), float32(y), float32(width), float32(height)) - vk.CmdSetViewport(cmdBuf, 0, vp) -} - -func (b *Backend) DrawArrays(off, count int) { - cmdBuf := b.currentCmdBuf() - if b.desc.dirty { - b.pipe.desc.bindDescriptorSet(b, cmdBuf, vk.PIPELINE_BIND_POINT_GRAPHICS, b.desc.texBinds, b.desc.bufBinds) - b.desc.dirty = false - } - vk.CmdDraw(cmdBuf, count, 1, off, 0) -} - -func (b *Backend) DrawElements(off, count int) { - cmdBuf := b.currentCmdBuf() - if b.desc.dirty { - b.pipe.desc.bindDescriptorSet(b, cmdBuf, vk.PIPELINE_BIND_POINT_GRAPHICS, b.desc.texBinds, b.desc.bufBinds) - b.desc.dirty = false - } - vk.CmdDrawIndexed(cmdBuf, count, 1, off, 0, 0) -} - -func (b *Backend) BindImageTexture(unit int, tex driver.Texture) { - t := tex.(*Texture) - b.desc.texBinds[unit] = t - b.desc.dirty = true - t.imageBarrier(b.currentCmdBuf(), - vk.IMAGE_LAYOUT_GENERAL, - vk.PIPELINE_STAGE_COMPUTE_SHADER_BIT, - vk.ACCESS_SHADER_READ_BIT|vk.ACCESS_SHADER_WRITE_BIT, - ) -} - -func (b *Backend) DispatchCompute(x, y, z int) { - cmdBuf := b.currentCmdBuf() - if b.desc.dirty { - b.pipe.desc.bindDescriptorSet(b, cmdBuf, vk.PIPELINE_BIND_POINT_COMPUTE, b.desc.texBinds, b.desc.bufBinds) - b.desc.dirty = false - } - vk.CmdDispatch(cmdBuf, x, y, z) -} - -func (t *Texture) Upload(offset, size image.Point, pixels []byte, stride int) { - if stride == 0 { - stride = size.X * 4 - } - cmdBuf := t.backend.ensureCmdBuf() - dstStride := size.X * 4 - n := size.Y * dstStride - stage, mem, off := t.backend.stagingBuffer(n) - var srcOff, dstOff int - for y := 0; y < size.Y; y++ { - srcRow := pixels[srcOff : srcOff+dstStride] - dstRow := mem[dstOff : dstOff+dstStride] - copy(dstRow, srcRow) - dstOff += dstStride - srcOff += stride - } - op := vk.BuildBufferImageCopy(off, dstStride/4, offset.X, offset.Y, size.X, size.Y) - t.imageBarrier(cmdBuf, - vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, - vk.PIPELINE_STAGE_TRANSFER_BIT, - vk.ACCESS_TRANSFER_WRITE_BIT, - ) - vk.CmdCopyBufferToImage(cmdBuf, stage.buf, t.img, t.layout, op) - // Build mipmaps by repeating linear blits. - w, h := t.width, t.height - for i := 1; i < t.mipmaps; i++ { - nw, nh := w/2, h/2 - if nh < 1 { - nh = 1 - } - if nw < 1 { - nw = 1 - } - // Transition previous (source) level. - b := vk.BuildImageMemoryBarrier( - t.img, - vk.ACCESS_TRANSFER_WRITE_BIT, vk.ACCESS_TRANSFER_READ_BIT, - vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - i-1, 1, - ) - vk.CmdPipelineBarrier(cmdBuf, vk.PIPELINE_STAGE_TRANSFER_BIT, vk.PIPELINE_STAGE_TRANSFER_BIT, vk.DEPENDENCY_BY_REGION_BIT, nil, nil, []vk.ImageMemoryBarrier{b}) - // Blit to this mipmap level. - blit := vk.BuildImageBlit(0, 0, 0, 0, w, h, nw, nh, i-1, i) - vk.CmdBlitImage(cmdBuf, t.img, vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, t.img, vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, []vk.ImageBlit{blit}, vk.FILTER_LINEAR) - w, h = nw, nh - } - if t.mipmaps > 1 { - // Add barrier for last blit. - b := vk.BuildImageMemoryBarrier( - t.img, - vk.ACCESS_TRANSFER_WRITE_BIT, vk.ACCESS_TRANSFER_READ_BIT, - vk.IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - t.mipmaps-1, 1, - ) - vk.CmdPipelineBarrier(cmdBuf, vk.PIPELINE_STAGE_TRANSFER_BIT, vk.PIPELINE_STAGE_TRANSFER_BIT, vk.DEPENDENCY_BY_REGION_BIT, nil, nil, []vk.ImageMemoryBarrier{b}) - t.layout = vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL - } -} - -func (t *Texture) Release() { - if t.foreign { - panic("external textures cannot be released") - } - freet := *t - t.backend.deferFunc(func(d vk.Device) { - if freet.fbo != 0 { - vk.DestroyFramebuffer(d, freet.fbo) - } - vk.DestroySampler(d, freet.sampler) - vk.DestroyImageView(d, freet.view) - vk.DestroyImage(d, freet.img) - vk.FreeMemory(d, freet.mem) - }) - *t = Texture{} -} - -func (p *Pipeline) Release() { - freep := *p - p.backend.deferFunc(func(d vk.Device) { - freep.desc.release(d) - vk.DestroyPipeline(d, freep.pipe) - }) - *p = Pipeline{} -} - -func (p *descPool) release(d vk.Device) { - if p := p.pool; p != 0 { - vk.DestroyDescriptorPool(d, p) - } - if l := p.descLayout; l != 0 { - vk.DestroyDescriptorSetLayout(d, l) - } - vk.DestroyPipelineLayout(d, p.layout) -} - -func (p *descPool) bindDescriptorSet(b *Backend, cmdBuf vk.CommandBuffer, bindPoint vk.PipelineBindPoint, texBinds [texUnits]*Texture, bufBinds [storageUnits]*Buffer) { - if p.size == len(p.sets) { - l := p.descLayout - if l == 0 { - panic("vulkan: descriptor set is dirty, but pipeline has empty layout") - } - newCap := len(p.sets) * 2 - if pool := p.pool; pool != 0 { - b.deferFunc(func(d vk.Device) { - vk.DestroyDescriptorPool(d, pool) - }) - } - const initialPoolSize = 100 - if newCap < initialPoolSize { - newCap = initialPoolSize - } - var poolSizes []vk.DescriptorPoolSize - if n := len(p.texBinds); n > 0 { - poolSizes = append(poolSizes, vk.BuildDescriptorPoolSize(vk.DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, newCap*n)) - } - if n := len(p.imgBinds); n > 0 { - poolSizes = append(poolSizes, vk.BuildDescriptorPoolSize(vk.DESCRIPTOR_TYPE_STORAGE_IMAGE, newCap*n)) - } - if n := len(p.bufBinds); n > 0 { - poolSizes = append(poolSizes, vk.BuildDescriptorPoolSize(vk.DESCRIPTOR_TYPE_STORAGE_BUFFER, newCap*n)) - } - pool, err := vk.CreateDescriptorPool(b.dev, newCap, poolSizes) - if err != nil { - panic(fmt.Errorf("vulkan: failed to allocate descriptor pool with %d descriptors: %v", newCap, err)) - } - p.pool = pool - sets, err := vk.AllocateDescriptorSets(b.dev, p.pool, l, newCap) - if err != nil { - panic(fmt.Errorf("vulkan: failed to allocate descriptor with %d sets: %v", newCap, err)) - } - p.sets = sets - p.size = 0 - } - descSet := p.sets[p.size] - p.size++ - for _, bind := range p.texBinds { - tex := texBinds[bind] - write := vk.BuildWriteDescriptorSetImage(descSet, bind, vk.DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, tex.sampler, tex.view, vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) - vk.UpdateDescriptorSet(b.dev, write) - } - for _, bind := range p.imgBinds { - tex := texBinds[bind] - write := vk.BuildWriteDescriptorSetImage(descSet, bind, vk.DESCRIPTOR_TYPE_STORAGE_IMAGE, 0, tex.view, vk.IMAGE_LAYOUT_GENERAL) - vk.UpdateDescriptorSet(b.dev, write) - } - for _, bind := range p.bufBinds { - buf := bufBinds[bind] - write := vk.BuildWriteDescriptorSetBuffer(descSet, bind, vk.DESCRIPTOR_TYPE_STORAGE_BUFFER, buf.buf) - vk.UpdateDescriptorSet(b.dev, write) - } - vk.CmdBindDescriptorSets(cmdBuf, bindPoint, p.layout, 0, []vk.DescriptorSet{descSet}) -} - -func (t *Texture) imageBarrier(cmdBuf vk.CommandBuffer, layout vk.ImageLayout, stage vk.PipelineStageFlags, access vk.AccessFlags) { - srcStage := t.scope.stage - if srcStage == 0 && t.layout == layout { - t.scope.stage = stage - t.scope.access = access - return - } - if srcStage == 0 { - srcStage = vk.PIPELINE_STAGE_TOP_OF_PIPE_BIT - } - b := vk.BuildImageMemoryBarrier( - t.img, - t.scope.access, access, - t.layout, layout, - 0, vk.REMAINING_MIP_LEVELS, - ) - vk.CmdPipelineBarrier(cmdBuf, srcStage, stage, vk.DEPENDENCY_BY_REGION_BIT, nil, nil, []vk.ImageMemoryBarrier{b}) - t.layout = layout - t.scope.stage = stage - t.scope.access = access -} - -func (b *Backend) PrepareTexture(tex driver.Texture) { - t := tex.(*Texture) - cmdBuf := b.ensureCmdBuf() - t.imageBarrier(cmdBuf, - vk.IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, - vk.PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - vk.ACCESS_SHADER_READ_BIT, - ) -} - -func (b *Backend) BindTexture(unit int, tex driver.Texture) { - t := tex.(*Texture) - b.desc.texBinds[unit] = t - b.desc.dirty = true -} - -func (b *Backend) BindPipeline(pipe driver.Pipeline) { - b.bindPipeline(pipe.(*Pipeline), vk.PIPELINE_BIND_POINT_GRAPHICS) -} - -func (b *Backend) BindProgram(prog driver.Program) { - b.bindPipeline(prog.(*Pipeline), vk.PIPELINE_BIND_POINT_COMPUTE) -} - -func (b *Backend) bindPipeline(p *Pipeline, point vk.PipelineBindPoint) { - b.pipe = p - b.desc.dirty = p.desc.descLayout != 0 - cmdBuf := b.currentCmdBuf() - vk.CmdBindPipeline(cmdBuf, point, p.pipe) -} - -func (s *Shader) Release() { - vk.DestroyShaderModule(s.dev, s.module) - *s = Shader{} -} - -func (b *Backend) BindStorageBuffer(binding int, buffer driver.Buffer) { - buf := buffer.(*Buffer) - b.desc.bufBinds[binding] = buf - b.desc.dirty = true - buf.barrier(b.currentCmdBuf(), - vk.PIPELINE_STAGE_COMPUTE_SHADER_BIT, - vk.ACCESS_SHADER_READ_BIT|vk.ACCESS_SHADER_WRITE_BIT, - ) -} - -func (b *Backend) BindUniforms(buffer driver.Buffer) { - buf := buffer.(*Buffer) - cmdBuf := b.currentCmdBuf() - for _, s := range b.pipe.pushRanges { - off := vk.PushConstantRangeOffset(s) - vk.CmdPushConstants(cmdBuf, b.pipe.desc.layout, vk.PushConstantRangeStageFlags(s), off, buf.store[off:off+vk.PushConstantRangeSize(s)]) - } -} - -func (b *Backend) BindVertexBuffer(buffer driver.Buffer, offset int) { - buf := buffer.(*Buffer) - cmdBuf := b.currentCmdBuf() - b.bindings = b.bindings[:0] - b.offsets = b.offsets[:0] - for i := 0; i < b.pipe.ninputs; i++ { - b.bindings = append(b.bindings, buf.buf) - b.offsets = append(b.offsets, vk.DeviceSize(offset)) - } - vk.CmdBindVertexBuffers(cmdBuf, 0, b.bindings, b.offsets) -} - -func (b *Backend) BindIndexBuffer(buffer driver.Buffer) { - buf := buffer.(*Buffer) - cmdBuf := b.currentCmdBuf() - vk.CmdBindIndexBuffer(cmdBuf, buf.buf, 0, vk.INDEX_TYPE_UINT16) -} - -func (b *Buffer) Download(data []byte) error { - if b.buf == 0 { - copy(data, b.store) - return nil - } - stage, mem, off := b.backend.stagingBuffer(len(data)) - cmdBuf := b.backend.ensureCmdBuf() - b.barrier(cmdBuf, - vk.PIPELINE_STAGE_TRANSFER_BIT, - vk.ACCESS_TRANSFER_READ_BIT, - ) - vk.CmdCopyBuffer(cmdBuf, b.buf, stage.buf, 0, off, len(data)) - stage.scope.stage = vk.PIPELINE_STAGE_TRANSFER_BIT - stage.scope.access = vk.ACCESS_TRANSFER_WRITE_BIT - stage.barrier(cmdBuf, - vk.PIPELINE_STAGE_HOST_BIT, - vk.ACCESS_HOST_READ_BIT, - ) - b.backend.submitCmdBuf(b.backend.fence) - vk.WaitForFences(b.backend.dev, b.backend.fence) - vk.ResetFences(b.backend.dev, b.backend.fence) - copy(data, mem) - return nil -} - -func (b *Buffer) Upload(data []byte) { - if b.buf == 0 { - copy(b.store, data) - return - } - stage, mem, off := b.backend.stagingBuffer(len(data)) - copy(mem, data) - cmdBuf := b.backend.ensureCmdBuf() - b.barrier(cmdBuf, - vk.PIPELINE_STAGE_TRANSFER_BIT, - vk.ACCESS_TRANSFER_WRITE_BIT, - ) - vk.CmdCopyBuffer(cmdBuf, stage.buf, b.buf, off, 0, len(data)) - var access vk.AccessFlags - if b.usage&vk.BUFFER_USAGE_INDEX_BUFFER_BIT != 0 { - access |= vk.ACCESS_INDEX_READ_BIT - } - if b.usage&vk.BUFFER_USAGE_VERTEX_BUFFER_BIT != 0 { - access |= vk.ACCESS_VERTEX_ATTRIBUTE_READ_BIT - } - if access != 0 { - b.barrier(cmdBuf, - vk.PIPELINE_STAGE_VERTEX_INPUT_BIT, - access, - ) - } -} - -func (b *Buffer) barrier(cmdBuf vk.CommandBuffer, stage vk.PipelineStageFlags, access vk.AccessFlags) { - srcStage := b.scope.stage - if srcStage == 0 { - b.scope.stage = stage - b.scope.access = access - return - } - barrier := vk.BuildBufferMemoryBarrier( - b.buf, - b.scope.access, access, - ) - vk.CmdPipelineBarrier(cmdBuf, srcStage, stage, vk.DEPENDENCY_BY_REGION_BIT, nil, []vk.BufferMemoryBarrier{barrier}, nil) - b.scope.stage = stage - b.scope.access = access -} - -func (b *Buffer) Release() { - freeb := *b - if freeb.buf != 0 { - b.backend.deferFunc(func(d vk.Device) { - vk.DestroyBuffer(d, freeb.buf) - vk.FreeMemory(d, freeb.mem) - }) - } - *b = Buffer{} -} - -func (t *Texture) ReadPixels(src image.Rectangle, pixels []byte, stride int) error { - if len(pixels) == 0 { - return nil - } - sz := src.Size() - stageStride := sz.X * 4 - n := sz.Y * stageStride - stage, mem, off := t.backend.stagingBuffer(n) - cmdBuf := t.backend.ensureCmdBuf() - region := vk.BuildBufferImageCopy(off, stageStride/4, src.Min.X, src.Min.Y, sz.X, sz.Y) - t.imageBarrier(cmdBuf, - vk.IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, - vk.PIPELINE_STAGE_TRANSFER_BIT, - vk.ACCESS_TRANSFER_READ_BIT, - ) - vk.CmdCopyImageToBuffer(cmdBuf, t.img, t.layout, stage.buf, []vk.BufferImageCopy{region}) - stage.scope.stage = vk.PIPELINE_STAGE_TRANSFER_BIT - stage.scope.access = vk.ACCESS_TRANSFER_WRITE_BIT - stage.barrier(cmdBuf, - vk.PIPELINE_STAGE_HOST_BIT, - vk.ACCESS_HOST_READ_BIT, - ) - t.backend.submitCmdBuf(t.backend.fence) - vk.WaitForFences(t.backend.dev, t.backend.fence) - vk.ResetFences(t.backend.dev, t.backend.fence) - var srcOff, dstOff int - for y := 0; y < sz.Y; y++ { - dstRow := pixels[srcOff : srcOff+stageStride] - srcRow := mem[dstOff : dstOff+stageStride] - copy(dstRow, srcRow) - dstOff += stageStride - srcOff += stride - } - return nil -} - -func (b *Backend) currentCmdBuf() vk.CommandBuffer { - cur := b.cmdPool.current - if cur == nil { - panic("vulkan: invalid operation outside a render or compute pass") - } - return cur -} - -func (b *Backend) ensureCmdBuf() vk.CommandBuffer { - if b.cmdPool.current != nil { - return b.cmdPool.current - } - if b.cmdPool.used < len(b.cmdPool.buffers) { - buf := b.cmdPool.buffers[b.cmdPool.used] - b.cmdPool.current = buf - } else { - buf, err := vk.AllocateCommandBuffer(b.dev, b.cmdPool.pool) - if err != nil { - panic(err) - } - b.cmdPool.buffers = append(b.cmdPool.buffers, buf) - b.cmdPool.current = buf - } - b.cmdPool.used++ - buf := b.cmdPool.current - if err := vk.BeginCommandBuffer(buf); err != nil { - panic(err) - } - return buf -} - -func (b *Backend) BeginRenderPass(tex driver.Texture, d driver.LoadDesc) { - t := tex.(*Texture) - var vkop vk.AttachmentLoadOp - switch d.Action { - case driver.LoadActionClear: - vkop = vk.ATTACHMENT_LOAD_OP_CLEAR - case driver.LoadActionInvalidate: - vkop = vk.ATTACHMENT_LOAD_OP_DONT_CARE - case driver.LoadActionKeep: - vkop = vk.ATTACHMENT_LOAD_OP_LOAD - } - cmdBuf := b.ensureCmdBuf() - if sem := t.acquire; sem != 0 { - // The render pass targets a framebuffer that has an associated acquire semaphore. - // Wait for it by forming an execution barrier. - b.waitSems = append(b.waitSems, sem) - b.waitStages = append(b.waitStages, vk.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT) - // But only for the first pass in a frame. - t.acquire = 0 - } - t.imageBarrier(cmdBuf, - vk.IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - vk.PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, - vk.ACCESS_COLOR_ATTACHMENT_READ_BIT|vk.ACCESS_COLOR_ATTACHMENT_WRITE_BIT, - ) - pass := b.lookupPass(t.format, vkop, t.layout, t.passLayout) - col := d.ClearColor - vk.CmdBeginRenderPass(cmdBuf, pass, t.fbo, t.width, t.height, [4]float32{col.R, col.G, col.B, col.A}) - t.layout = t.passLayout - // If the render pass describes an automatic image layout transition to its final layout, there - // is an implicit image barrier with destination PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT. Make - // sure any subsequent barrier includes the transition. - // See also https://www.khronos.org/registry/vulkan/specs/1.0/html/vkspec.html#VkSubpassDependency. - t.scope.stage |= vk.PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT -} - -func (b *Backend) EndRenderPass() { - vk.CmdEndRenderPass(b.cmdPool.current) -} - -func (b *Backend) BeginCompute() { - b.ensureCmdBuf() -} - -func (b *Backend) EndCompute() { -} - -func (b *Backend) lookupPass(fmt vk.Format, loadAct vk.AttachmentLoadOp, initLayout, finalLayout vk.ImageLayout) vk.RenderPass { - key := passKey{fmt: fmt, loadAct: loadAct, initLayout: initLayout, finalLayout: finalLayout} - if pass, ok := b.passes[key]; ok { - return pass - } - pass, err := vk.CreateRenderPass(b.dev, fmt, loadAct, initLayout, finalLayout, nil) - if err != nil { - panic(err) - } - b.passes[key] = pass - return pass -} - -func (b *Backend) submitCmdBuf(fence vk.Fence) { - buf := b.cmdPool.current - if buf == nil && fence == 0 { - return - } - buf = b.ensureCmdBuf() - b.cmdPool.current = nil - if err := vk.EndCommandBuffer(buf); err != nil { - panic(err) - } - if err := vk.QueueSubmit(b.queue, buf, b.waitSems, b.waitStages, b.sigSems, fence); err != nil { - panic(err) - } - b.waitSems = b.waitSems[:0] - b.sigSems = b.sigSems[:0] - b.waitStages = b.waitStages[:0] -} - -func (b *Backend) stagingBuffer(size int) (*Buffer, []byte, int) { - if b.staging.size+size > b.staging.cap { - if b.staging.buf != nil { - vk.UnmapMemory(b.dev, b.staging.buf.mem) - b.staging.buf.Release() - b.staging.cap = 0 - } - cap := 2 * (b.staging.size + size) - buf, err := b.newBuffer(cap, vk.BUFFER_USAGE_TRANSFER_SRC_BIT|vk.BUFFER_USAGE_TRANSFER_DST_BIT, - vk.MEMORY_PROPERTY_HOST_VISIBLE_BIT|vk.MEMORY_PROPERTY_HOST_COHERENT_BIT) - if err != nil { - panic(err) - } - mem, err := vk.MapMemory(b.dev, buf.mem, 0, cap) - if err != nil { - buf.Release() - panic(err) - } - b.staging.buf = buf - b.staging.mem = mem - b.staging.size = 0 - b.staging.cap = cap - } - off := b.staging.size - b.staging.size += size - mem := b.staging.mem[off : off+size] - return b.staging.buf, mem, off -} - -func formatFor(format driver.TextureFormat) vk.Format { - switch format { - case driver.TextureFormatRGBA8: - return vk.FORMAT_R8G8B8A8_UNORM - case driver.TextureFormatSRGBA: - return vk.FORMAT_R8G8B8A8_SRGB - case driver.TextureFormatFloat: - return vk.FORMAT_R16_SFLOAT - default: - panic("unsupported texture format") - } -} - -func mapErr(err error) error { - var vkErr vk.Error - if errors.As(err, &vkErr) && vkErr == vk.ERROR_DEVICE_LOST { - return driver.ErrDeviceLost - } - return err -} - -func (f *Texture) ImplementsRenderTarget() {} diff --git a/gio/gpu/internal/vulkan/vulkan_nosupport.go b/gio/gpu/internal/vulkan/vulkan_nosupport.go deleted file mode 100644 index 4364a43..0000000 --- a/gio/gpu/internal/vulkan/vulkan_nosupport.go +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package vulkan - -// Empty file to avoid the build error for platforms without Vulkan support. diff --git a/gio/gpu/pack.go b/gio/gpu/pack.go deleted file mode 100644 index cb0ca02..0000000 --- a/gio/gpu/pack.go +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gpu - -import ( - "image" -) - -// packer packs a set of many smaller rectangles into -// much fewer larger atlases. -type packer struct { - maxDims image.Point - spaces []image.Rectangle - - sizes []image.Point - pos image.Point -} - -type placement struct { - Idx int - Pos image.Point -} - -// add adds the given rectangle to the atlases and -// return the allocated position. -func (p *packer) add(s image.Point) (placement, bool) { - if place, ok := p.tryAdd(s); ok { - return place, true - } - p.newPage() - return p.tryAdd(s) -} - -func (p *packer) clear() { - p.sizes = p.sizes[:0] - p.spaces = p.spaces[:0] -} - -func (p *packer) newPage() { - p.pos = image.Point{} - p.sizes = append(p.sizes, image.Point{}) - p.spaces = p.spaces[:0] - p.spaces = append(p.spaces, image.Rectangle{ - Max: image.Point{X: 1e6, Y: 1e6}, - }) -} - -func (p *packer) tryAdd(s image.Point) (placement, bool) { - if len(p.spaces) == 0 || len(p.sizes) == 0 { - return placement{}, false - } - - var ( - bestIdx *image.Rectangle - bestSize = p.maxDims - lastSize = p.sizes[len(p.sizes)-1] - ) - // Go backwards to prioritize smaller spaces. - for i := range p.spaces { - space := &p.spaces[i] - rightSpace := space.Dx() - s.X - bottomSpace := space.Dy() - s.Y - if rightSpace < 0 || bottomSpace < 0 { - continue - } - size := lastSize - if x := space.Min.X + s.X; x > size.X { - if x > p.maxDims.X { - continue - } - size.X = x - } - if y := space.Min.Y + s.Y; y > size.Y { - if y > p.maxDims.Y { - continue - } - size.Y = y - } - if size.X*size.Y < bestSize.X*bestSize.Y { - bestIdx = space - bestSize = size - } - } - if bestIdx == nil { - return placement{}, false - } - // Remove space. - bestSpace := *bestIdx - *bestIdx = p.spaces[len(p.spaces)-1] - p.spaces = p.spaces[:len(p.spaces)-1] - // Put s in the top left corner and add the (at most) - // two smaller spaces. - pos := bestSpace.Min - if rem := bestSpace.Dy() - s.Y; rem > 0 { - p.spaces = append(p.spaces, image.Rectangle{ - Min: image.Point{X: pos.X, Y: pos.Y + s.Y}, - Max: image.Point{X: bestSpace.Max.X, Y: bestSpace.Max.Y}, - }) - } - if rem := bestSpace.Dx() - s.X; rem > 0 { - p.spaces = append(p.spaces, image.Rectangle{ - Min: image.Point{X: pos.X + s.X, Y: pos.Y}, - Max: image.Point{X: bestSpace.Max.X, Y: pos.Y + s.Y}, - }) - } - idx := len(p.sizes) - 1 - p.sizes[idx] = bestSize - return placement{Idx: idx, Pos: pos}, true -} diff --git a/gio/gpu/pack_test.go b/gio/gpu/pack_test.go deleted file mode 100644 index e0db75c..0000000 --- a/gio/gpu/pack_test.go +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gpu - -import ( - "image" - "testing" -) - -func BenchmarkPacker(b *testing.B) { - var p packer - p.maxDims = image.Point{X: 4096, Y: 4096} - for i := 0; b.Loop(); i++ { - p.clear() - p.newPage() - for k := range 500 { - _, ok := p.tryAdd(xy(k)) - if !ok { - b.Fatal("add failed", i, k, xy(k)) - } - } - } -} - -func xy(v int) image.Point { - return image.Point{ - X: ((v / 16) % 16) + 8, - Y: (v % 16) + 8, - } -} diff --git a/gio/gpu/path.go b/gio/gpu/path.go deleted file mode 100644 index 0369079..0000000 --- a/gio/gpu/path.go +++ /dev/null @@ -1,424 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gpu - -// GPU accelerated path drawing using the algorithms from -// Pathfinder (https://github.com/servo/pathfinder). - -import ( - "encoding/binary" - "image" - "math" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" - "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" - "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/shader" - "github.com/p9c/p9/pkg/gel/gio/shader/gio" -) - -type pather struct { - ctx driver.Device - - viewport image.Point - - stenciler *stenciler - coverer *coverer -} - -type coverer struct { - ctx driver.Device - pipelines [2][3]*pipeline - texUniforms *coverTexUniforms - colUniforms *coverColUniforms - linearGradientUniforms *coverLinearGradientUniforms -} - -type coverTexUniforms struct { - coverUniforms - _ [12]byte // Padding to multiple of 16. -} - -type coverColUniforms struct { - coverUniforms - _ [128 - unsafe.Sizeof(coverUniforms{}) - unsafe.Sizeof(colorUniforms{})]byte // Padding to 128 bytes. - colorUniforms -} - -type coverLinearGradientUniforms struct { - coverUniforms - _ [128 - unsafe.Sizeof(coverUniforms{}) - unsafe.Sizeof(gradientUniforms{})]byte // Padding to 128. - gradientUniforms -} - -type coverUniforms struct { - transform [4]float32 - uvCoverTransform [4]float32 - uvTransformR1 [4]float32 - uvTransformR2 [4]float32 - fbo float32 -} - -type stenciler struct { - ctx driver.Device - pipeline struct { - pipeline *pipeline - uniforms *stencilUniforms - } - ipipeline struct { - pipeline *pipeline - uniforms *intersectUniforms - } - fbos fboSet - intersections fboSet - indexBuf driver.Buffer -} - -type stencilUniforms struct { - transform [4]float32 - pathOffset [2]float32 - _ [8]byte // Padding to multiple of 16. -} - -type intersectUniforms struct { - vert struct { - uvTransform [4]float32 - subUVTransform [4]float32 - } -} - -type fboSet struct { - fbos []FBO -} - -type FBO struct { - size image.Point - tex driver.Texture -} - -type pathData struct { - ncurves int - data driver.Buffer -} - -// vertex data suitable for passing to vertex programs. -type vertex struct { - // Corner encodes the corner: +0.5 for south, +.25 for east. - Corner float32 - MaxY float32 - FromX, FromY float32 - CtrlX, CtrlY float32 - ToX, ToY float32 -} - -// encode needs to stay in-sync with the code in clip.go encodeQuadTo. -func (v vertex) encode(d []byte, maxy uint32) { - d = d[0:32] - bo := binary.LittleEndian - bo.PutUint32(d[0:4], math.Float32bits(v.Corner)) - bo.PutUint32(d[4:8], maxy) - bo.PutUint32(d[8:12], math.Float32bits(v.FromX)) - bo.PutUint32(d[12:16], math.Float32bits(v.FromY)) - bo.PutUint32(d[16:20], math.Float32bits(v.CtrlX)) - bo.PutUint32(d[20:24], math.Float32bits(v.CtrlY)) - bo.PutUint32(d[24:28], math.Float32bits(v.ToX)) - bo.PutUint32(d[28:32], math.Float32bits(v.ToY)) -} - -const ( - // Number of path quads per draw batch. - pathBatchSize = 10000 - // Size of a vertex as sent to gpu - vertStride = 8 * 4 -) - -func newPather(ctx driver.Device) *pather { - return &pather{ - ctx: ctx, - stenciler: newStenciler(ctx), - coverer: newCoverer(ctx), - } -} - -func newCoverer(ctx driver.Device) *coverer { - c := &coverer{ - ctx: ctx, - } - c.colUniforms = new(coverColUniforms) - c.texUniforms = new(coverTexUniforms) - c.linearGradientUniforms = new(coverLinearGradientUniforms) - pipelines, err := createColorPrograms(ctx, gio.Shader_cover_vert, gio.Shader_cover_frag, - [3]any{c.colUniforms, c.linearGradientUniforms, c.texUniforms}, - ) - if err != nil { - panic(err) - } - c.pipelines = pipelines - return c -} - -func newStenciler(ctx driver.Device) *stenciler { - // Allocate a suitably large index buffer for drawing paths. - indices := make([]uint16, pathBatchSize*6) - for i := range pathBatchSize { - i := uint16(i) - indices[i*6+0] = i*4 + 0 - indices[i*6+1] = i*4 + 1 - indices[i*6+2] = i*4 + 2 - indices[i*6+3] = i*4 + 2 - indices[i*6+4] = i*4 + 1 - indices[i*6+5] = i*4 + 3 - } - indexBuf, err := ctx.NewImmutableBuffer(driver.BufferBindingIndices, byteslice.Slice(indices)) - if err != nil { - panic(err) - } - progLayout := driver.VertexLayout{ - Inputs: []driver.InputDesc{ - {Type: shader.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))}, - {Type: shader.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))}, - {Type: shader.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))}, - {Type: shader.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))}, - {Type: shader.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))}, - }, - Stride: vertStride, - } - iprogLayout := driver.VertexLayout{ - Inputs: []driver.InputDesc{ - {Type: shader.DataTypeFloat, Size: 2, Offset: 0}, - {Type: shader.DataTypeFloat, Size: 2, Offset: 4 * 2}, - }, - Stride: 4 * 4, - } - st := &stenciler{ - ctx: ctx, - indexBuf: indexBuf, - } - vsh, fsh, err := newShaders(ctx, gio.Shader_stencil_vert, gio.Shader_stencil_frag) - if err != nil { - panic(err) - } - defer vsh.Release() - defer fsh.Release() - st.pipeline.uniforms = new(stencilUniforms) - vertUniforms := newUniformBuffer(ctx, st.pipeline.uniforms) - pipe, err := st.ctx.NewPipeline(driver.PipelineDesc{ - VertexShader: vsh, - FragmentShader: fsh, - VertexLayout: progLayout, - BlendDesc: driver.BlendDesc{ - Enable: true, - SrcFactor: driver.BlendFactorOne, - DstFactor: driver.BlendFactorOne, - }, - PixelFormat: driver.TextureFormatFloat, - Topology: driver.TopologyTriangles, - }) - st.pipeline.pipeline = &pipeline{pipe, vertUniforms} - if err != nil { - panic(err) - } - vsh, fsh, err = newShaders(ctx, gio.Shader_intersect_vert, gio.Shader_intersect_frag) - if err != nil { - panic(err) - } - defer vsh.Release() - defer fsh.Release() - st.ipipeline.uniforms = new(intersectUniforms) - vertUniforms = newUniformBuffer(ctx, &st.ipipeline.uniforms.vert) - ipipe, err := st.ctx.NewPipeline(driver.PipelineDesc{ - VertexShader: vsh, - FragmentShader: fsh, - VertexLayout: iprogLayout, - BlendDesc: driver.BlendDesc{ - Enable: true, - SrcFactor: driver.BlendFactorDstColor, - DstFactor: driver.BlendFactorZero, - }, - PixelFormat: driver.TextureFormatFloat, - Topology: driver.TopologyTriangleStrip, - }) - st.ipipeline.pipeline = &pipeline{ipipe, vertUniforms} - if err != nil { - panic(err) - } - return st -} - -func (s *fboSet) resize(ctx driver.Device, format driver.TextureFormat, sizes []image.Point) { - // Add fbos. - for i := len(s.fbos); i < len(sizes); i++ { - s.fbos = append(s.fbos, FBO{}) - } - // Resize fbos. - for i, sz := range sizes { - f := &s.fbos[i] - // Resizing or recreating FBOs can introduce rendering stalls. - // Avoid if the space waste is not too high. - resize := sz.X > f.size.X || sz.Y > f.size.Y - waste := float32(sz.X*sz.Y) / float32(f.size.X*f.size.Y) - resize = resize || waste > 1.2 - if resize { - if f.tex != nil { - f.tex.Release() - } - // Add 5% extra space in each dimension to minimize resizing. - sz = sz.Mul(105).Div(100) - max := ctx.Caps().MaxTextureSize - if sz.Y > max { - sz.Y = max - } - if sz.X > max { - sz.X = max - } - tex, err := ctx.NewTexture(format, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest, - driver.BufferBindingTexture|driver.BufferBindingFramebuffer) - if err != nil { - panic(err) - } - f.size = sz - f.tex = tex - } - } - // Delete extra fbos. - s.delete(ctx, len(sizes)) -} - -func (s *fboSet) delete(ctx driver.Device, idx int) { - for i := idx; i < len(s.fbos); i++ { - f := s.fbos[i] - f.tex.Release() - } - s.fbos = s.fbos[:idx] -} - -func (s *stenciler) release() { - s.fbos.delete(s.ctx, 0) - s.intersections.delete(s.ctx, 0) - s.pipeline.pipeline.Release() - s.ipipeline.pipeline.Release() - s.indexBuf.Release() -} - -func (p *pather) release() { - p.stenciler.release() - p.coverer.release() -} - -func (c *coverer) release() { - for _, p := range c.pipelines { - for _, p := range p { - p.Release() - } - } -} - -func buildPath(ctx driver.Device, p []byte) pathData { - buf, err := ctx.NewImmutableBuffer(driver.BufferBindingVertices, p) - if err != nil { - panic(err) - } - return pathData{ - ncurves: len(p) / vertStride, - data: buf, - } -} - -func (p pathData) release() { - p.data.Release() -} - -func (p *pather) begin(sizes []image.Point) { - p.stenciler.begin(sizes) -} - -func (p *pather) stencilPath(bounds image.Rectangle, offset image.Point, uv image.Point, data pathData) { - p.stenciler.stencilPath(bounds, offset, uv, data) -} - -func (s *stenciler) beginIntersect(sizes []image.Point) { - // 8 bit coverage is enough, but OpenGL ES only supports single channel - // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if - // no floating point support is available. - s.intersections.resize(s.ctx, driver.TextureFormatFloat, sizes) -} - -func (s *stenciler) cover(idx int) FBO { - return s.fbos.fbos[idx] -} - -func (s *stenciler) begin(sizes []image.Point) { - s.fbos.resize(s.ctx, driver.TextureFormatFloat, sizes) -} - -func (s *stenciler) stencilPath(bounds image.Rectangle, offset image.Point, uv image.Point, data pathData) { - s.ctx.Viewport(uv.X, uv.Y, bounds.Dx(), bounds.Dy()) - // Transform UI coordinates to OpenGL coordinates. - texSize := f32.Point{X: float32(bounds.Dx()), Y: float32(bounds.Dy())} - scale := f32.Point{X: 2 / texSize.X, Y: 2 / texSize.Y} - orig := f32.Point{X: -1 - float32(bounds.Min.X)*2/texSize.X, Y: -1 - float32(bounds.Min.Y)*2/texSize.Y} - s.pipeline.uniforms.transform = [4]float32{scale.X, scale.Y, orig.X, orig.Y} - s.pipeline.uniforms.pathOffset = [2]float32{float32(offset.X), float32(offset.Y)} - s.pipeline.pipeline.UploadUniforms(s.ctx) - // Draw in batches that fit in uint16 indices. - start := 0 - nquads := data.ncurves / 4 - for start < nquads { - batch := nquads - start - if max := pathBatchSize; batch > max { - batch = max - } - off := vertStride * start * 4 - s.ctx.BindVertexBuffer(data.data, off) - s.ctx.DrawElements(0, batch*6) - start += batch - } -} - -func (p *pather) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) { - p.coverer.cover(mat, isFBO, col, col1, col2, scale, off, uvTrans, coverScale, coverOff) -} - -func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) { - var uniforms *coverUniforms - switch mat { - case materialColor: - c.colUniforms.color = col - uniforms = &c.colUniforms.coverUniforms - case materialLinearGradient: - c.linearGradientUniforms.color1 = col1 - c.linearGradientUniforms.color2 = col2 - - t1, t2, t3, t4, t5, t6 := uvTrans.Elems() - c.linearGradientUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} - c.linearGradientUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} - uniforms = &c.linearGradientUniforms.coverUniforms - case materialTexture: - t1, t2, t3, t4, t5, t6 := uvTrans.Elems() - c.texUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0} - c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} - uniforms = &c.texUniforms.coverUniforms - } - uniforms.fbo = 0 - if isFBO { - uniforms.fbo = 1 - } - uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} - uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y} - fboIdx := 0 - if isFBO { - fboIdx = 1 - } - c.pipelines[fboIdx][mat].UploadUniforms(c.ctx) - c.ctx.DrawArrays(0, 4) -} - -func init() { - // Check that struct vertex has the expected size and - // that it contains no padding. - if unsafe.Sizeof(*(*vertex)(nil)) != vertStride { - panic("unexpected struct size") - } -} diff --git a/gio/gpu/timer.go b/gio/gpu/timer.go deleted file mode 100644 index fd0144d..0000000 --- a/gio/gpu/timer.go +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gpu - -import ( - "time" - - "github.com/p9c/p9/pkg/gel/gio/gpu/internal/driver" -) - -type timers struct { - backend driver.Device - timers []*timer -} - -type timer struct { - Elapsed time.Duration - backend driver.Device - timer driver.Timer - state timerState -} - -type timerState uint8 - -const ( - timerIdle timerState = iota - timerRunning - timerWaiting -) - -func newTimers(b driver.Device) *timers { - return &timers{ - backend: b, - } -} - -func (t *timers) newTimer() *timer { - if t == nil { - return nil - } - tt := &timer{ - backend: t.backend, - timer: t.backend.NewTimer(), - } - t.timers = append(t.timers, tt) - return tt -} - -func (t *timer) begin() { - if t == nil || t.state != timerIdle { - return - } - t.timer.Begin() - t.state = timerRunning -} - -func (t *timer) end() { - if t == nil || t.state != timerRunning { - return - } - t.timer.End() - t.state = timerWaiting -} - -func (t *timers) ready() bool { - if t == nil { - return false - } - for _, tt := range t.timers { - switch tt.state { - case timerIdle: - continue - case timerRunning: - return false - } - d, ok := tt.timer.Duration() - if !ok { - return false - } - tt.state = timerIdle - tt.Elapsed = d - } - return t.backend.IsTimeContinuous() -} - -func (t *timers) Release() { - if t == nil { - return - } - for _, tt := range t.timers { - tt.timer.Release() - } - t.timers = nil -} diff --git a/gio/internal/byteslice/byteslice.go b/gio/internal/byteslice/byteslice.go deleted file mode 100644 index 5bff4e3..0000000 --- a/gio/internal/byteslice/byteslice.go +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package byteslice provides byte slice views of other Go values such as -// slices and structs. -package byteslice - -import ( - "reflect" - "unsafe" -) - -// Struct returns a byte slice view of a struct. -func Struct(s any) []byte { - v := reflect.ValueOf(s) - sz := int(v.Elem().Type().Size()) - return unsafe.Slice((*byte)(unsafe.Pointer(v.Pointer())), sz) -} - -// Uint32 returns a byte slice view of a uint32 slice. -func Uint32(s []uint32) []byte { - n := len(s) - if n == 0 { - return nil - } - blen := n * int(unsafe.Sizeof(s[0])) - return unsafe.Slice((*byte)(unsafe.Pointer(&s[0])), blen) -} - -// Slice returns a byte slice view of a slice. -func Slice(s any) []byte { - v := reflect.ValueOf(s) - first := v.Index(0) - sz := int(first.Type().Size()) - res := unsafe.Slice((*byte)(unsafe.Pointer(v.Pointer())), sz*v.Cap()) - return res[:sz*v.Len()] -} diff --git a/gio/internal/cocoainit/cocoa_darwin.go b/gio/internal/cocoainit/cocoa_darwin.go deleted file mode 100644 index aa2fc6e..0000000 --- a/gio/internal/cocoainit/cocoa_darwin.go +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package cocoainit initializes support for multithreaded -// programs in Cocoa. -package cocoainit - -/* -#cgo CFLAGS: -xobjective-c -fobjc-arc -#cgo LDFLAGS: -framework Foundation -#import - -static inline void activate_cocoa_multithreading() { - [[NSThread new] start]; -} -#pragma GCC visibility push(hidden) -*/ -import "C" - -func init() { - C.activate_cocoa_multithreading() -} diff --git a/gio/internal/d3d11/d3d11_windows.go b/gio/internal/d3d11/d3d11_windows.go deleted file mode 100644 index 32aa689..0000000 --- a/gio/internal/d3d11/d3d11_windows.go +++ /dev/null @@ -1,1694 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package d3d11 - -import ( - "fmt" - "math" - "syscall" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - - "golang.org/x/sys/windows" -) - -type DXGI_SWAP_CHAIN_DESC struct { - BufferDesc DXGI_MODE_DESC - SampleDesc DXGI_SAMPLE_DESC - BufferUsage uint32 - BufferCount uint32 - OutputWindow windows.Handle - Windowed uint32 - SwapEffect uint32 - Flags uint32 -} - -type DXGI_SAMPLE_DESC struct { - Count uint32 - Quality uint32 -} - -type DXGI_MODE_DESC struct { - Width uint32 - Height uint32 - RefreshRate DXGI_RATIONAL - Format uint32 - ScanlineOrdering uint32 - Scaling uint32 -} - -type DXGI_RATIONAL struct { - Numerator uint32 - Denominator uint32 -} - -type TEXTURE2D_DESC struct { - Width uint32 - Height uint32 - MipLevels uint32 - ArraySize uint32 - Format uint32 - SampleDesc DXGI_SAMPLE_DESC - Usage uint32 - BindFlags uint32 - CPUAccessFlags uint32 - MiscFlags uint32 -} - -type SAMPLER_DESC struct { - Filter uint32 - AddressU uint32 - AddressV uint32 - AddressW uint32 - MipLODBias float32 - MaxAnisotropy uint32 - ComparisonFunc uint32 - BorderColor [4]float32 - MinLOD float32 - MaxLOD float32 -} - -type SHADER_RESOURCE_VIEW_DESC_TEX2D struct { - SHADER_RESOURCE_VIEW_DESC - Texture2D TEX2D_SRV -} - -type SHADER_RESOURCE_VIEW_DESC_BUFFEREX struct { - SHADER_RESOURCE_VIEW_DESC - Buffer BUFFEREX_SRV -} - -type UNORDERED_ACCESS_VIEW_DESC_TEX2D struct { - UNORDERED_ACCESS_VIEW_DESC - Texture2D TEX2D_UAV -} - -type UNORDERED_ACCESS_VIEW_DESC_BUFFER struct { - UNORDERED_ACCESS_VIEW_DESC - Buffer BUFFER_UAV -} - -type SHADER_RESOURCE_VIEW_DESC struct { - Format uint32 - ViewDimension uint32 -} - -type UNORDERED_ACCESS_VIEW_DESC struct { - Format uint32 - ViewDimension uint32 -} - -type TEX2D_SRV struct { - MostDetailedMip uint32 - MipLevels uint32 -} - -type BUFFEREX_SRV struct { - FirstElement uint32 - NumElements uint32 - Flags uint32 -} - -type TEX2D_UAV struct { - MipSlice uint32 -} - -type BUFFER_UAV struct { - FirstElement uint32 - NumElements uint32 - Flags uint32 -} - -type INPUT_ELEMENT_DESC struct { - SemanticName *byte - SemanticIndex uint32 - Format uint32 - InputSlot uint32 - AlignedByteOffset uint32 - InputSlotClass uint32 - InstanceDataStepRate uint32 -} - -type IDXGISwapChain struct { - Vtbl *struct { - _IUnknownVTbl - SetPrivateData uintptr - SetPrivateDataInterface uintptr - GetPrivateData uintptr - GetParent uintptr - GetDevice uintptr - Present uintptr - GetBuffer uintptr - SetFullscreenState uintptr - GetFullscreenState uintptr - GetDesc uintptr - ResizeBuffers uintptr - ResizeTarget uintptr - GetContainingOutput uintptr - GetFrameStatistics uintptr - GetLastPresentCount uintptr - } -} - -type Debug struct { - Vtbl *struct { - _IUnknownVTbl - SetFeatureMask uintptr - GetFeatureMask uintptr - SetPresentPerRenderOpDelay uintptr - GetPresentPerRenderOpDelay uintptr - SetSwapChain uintptr - GetSwapChain uintptr - ValidateContext uintptr - ReportLiveDeviceObjects uintptr - ValidateContextForDispatch uintptr - } -} - -type Device struct { - Vtbl *struct { - _IUnknownVTbl - CreateBuffer uintptr - CreateTexture1D uintptr - CreateTexture2D uintptr - CreateTexture3D uintptr - CreateShaderResourceView uintptr - CreateUnorderedAccessView uintptr - CreateRenderTargetView uintptr - CreateDepthStencilView uintptr - CreateInputLayout uintptr - CreateVertexShader uintptr - CreateGeometryShader uintptr - CreateGeometryShaderWithStreamOutput uintptr - CreatePixelShader uintptr - CreateHullShader uintptr - CreateDomainShader uintptr - CreateComputeShader uintptr - CreateClassLinkage uintptr - CreateBlendState uintptr - CreateDepthStencilState uintptr - CreateRasterizerState uintptr - CreateSamplerState uintptr - CreateQuery uintptr - CreatePredicate uintptr - CreateCounter uintptr - CreateDeferredContext uintptr - OpenSharedResource uintptr - CheckFormatSupport uintptr - CheckMultisampleQualityLevels uintptr - CheckCounterInfo uintptr - CheckCounter uintptr - CheckFeatureSupport uintptr - GetPrivateData uintptr - SetPrivateData uintptr - SetPrivateDataInterface uintptr - GetFeatureLevel uintptr - GetCreationFlags uintptr - GetDeviceRemovedReason uintptr - GetImmediateContext uintptr - SetExceptionMode uintptr - GetExceptionMode uintptr - } -} - -type DeviceContext struct { - Vtbl *struct { - _IUnknownVTbl - GetDevice uintptr - GetPrivateData uintptr - SetPrivateData uintptr - SetPrivateDataInterface uintptr - VSSetConstantBuffers uintptr - PSSetShaderResources uintptr - PSSetShader uintptr - PSSetSamplers uintptr - VSSetShader uintptr - DrawIndexed uintptr - Draw uintptr - Map uintptr - Unmap uintptr - PSSetConstantBuffers uintptr - IASetInputLayout uintptr - IASetVertexBuffers uintptr - IASetIndexBuffer uintptr - DrawIndexedInstanced uintptr - DrawInstanced uintptr - GSSetConstantBuffers uintptr - GSSetShader uintptr - IASetPrimitiveTopology uintptr - VSSetShaderResources uintptr - VSSetSamplers uintptr - Begin uintptr - End uintptr - GetData uintptr - SetPredication uintptr - GSSetShaderResources uintptr - GSSetSamplers uintptr - OMSetRenderTargets uintptr - OMSetRenderTargetsAndUnorderedAccessViews uintptr - OMSetBlendState uintptr - OMSetDepthStencilState uintptr - SOSetTargets uintptr - DrawAuto uintptr - DrawIndexedInstancedIndirect uintptr - DrawInstancedIndirect uintptr - Dispatch uintptr - DispatchIndirect uintptr - RSSetState uintptr - RSSetViewports uintptr - RSSetScissorRects uintptr - CopySubresourceRegion uintptr - CopyResource uintptr - UpdateSubresource uintptr - CopyStructureCount uintptr - ClearRenderTargetView uintptr - ClearUnorderedAccessViewUint uintptr - ClearUnorderedAccessViewFloat uintptr - ClearDepthStencilView uintptr - GenerateMips uintptr - SetResourceMinLOD uintptr - GetResourceMinLOD uintptr - ResolveSubresource uintptr - ExecuteCommandList uintptr - HSSetShaderResources uintptr - HSSetShader uintptr - HSSetSamplers uintptr - HSSetConstantBuffers uintptr - DSSetShaderResources uintptr - DSSetShader uintptr - DSSetSamplers uintptr - DSSetConstantBuffers uintptr - CSSetShaderResources uintptr - CSSetUnorderedAccessViews uintptr - CSSetShader uintptr - CSSetSamplers uintptr - CSSetConstantBuffers uintptr - VSGetConstantBuffers uintptr - PSGetShaderResources uintptr - PSGetShader uintptr - PSGetSamplers uintptr - VSGetShader uintptr - PSGetConstantBuffers uintptr - IAGetInputLayout uintptr - IAGetVertexBuffers uintptr - IAGetIndexBuffer uintptr - GSGetConstantBuffers uintptr - GSGetShader uintptr - IAGetPrimitiveTopology uintptr - VSGetShaderResources uintptr - VSGetSamplers uintptr - GetPredication uintptr - GSGetShaderResources uintptr - GSGetSamplers uintptr - OMGetRenderTargets uintptr - OMGetRenderTargetsAndUnorderedAccessViews uintptr - OMGetBlendState uintptr - OMGetDepthStencilState uintptr - SOGetTargets uintptr - RSGetState uintptr - RSGetViewports uintptr - RSGetScissorRects uintptr - HSGetShaderResources uintptr - HSGetShader uintptr - HSGetSamplers uintptr - HSGetConstantBuffers uintptr - DSGetShaderResources uintptr - DSGetShader uintptr - DSGetSamplers uintptr - DSGetConstantBuffers uintptr - CSGetShaderResources uintptr - CSGetUnorderedAccessViews uintptr - CSGetShader uintptr - CSGetSamplers uintptr - CSGetConstantBuffers uintptr - ClearState uintptr - Flush uintptr - GetType uintptr - GetContextFlags uintptr - FinishCommandList uintptr - } -} - -type RenderTargetView struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type Resource struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type Texture2D struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type Buffer struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type SamplerState struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type PixelShader struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type ShaderResourceView struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type UnorderedAccessView struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type DepthStencilView struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type BlendState struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type DepthStencilState struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type VertexShader struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type ComputeShader struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type RasterizerState struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type InputLayout struct { - Vtbl *struct { - _IUnknownVTbl - GetBufferPointer uintptr - GetBufferSize uintptr - } -} - -type DEPTH_STENCIL_DESC struct { - DepthEnable uint32 - DepthWriteMask uint32 - DepthFunc uint32 - StencilEnable uint32 - StencilReadMask uint8 - StencilWriteMask uint8 - FrontFace DEPTH_STENCILOP_DESC - BackFace DEPTH_STENCILOP_DESC -} - -type DEPTH_STENCILOP_DESC struct { - StencilFailOp uint32 - StencilDepthFailOp uint32 - StencilPassOp uint32 - StencilFunc uint32 -} - -type DEPTH_STENCIL_VIEW_DESC_TEX2D struct { - Format uint32 - ViewDimension uint32 - Flags uint32 - Texture2D TEX2D_DSV -} - -type TEX2D_DSV struct { - MipSlice uint32 -} - -type BLEND_DESC struct { - AlphaToCoverageEnable uint32 - IndependentBlendEnable uint32 - RenderTarget [8]RENDER_TARGET_BLEND_DESC -} - -type RENDER_TARGET_BLEND_DESC struct { - BlendEnable uint32 - SrcBlend uint32 - DestBlend uint32 - BlendOp uint32 - SrcBlendAlpha uint32 - DestBlendAlpha uint32 - BlendOpAlpha uint32 - RenderTargetWriteMask uint8 -} - -type IDXGIObject struct { - Vtbl *struct { - _IUnknownVTbl - SetPrivateData uintptr - SetPrivateDataInterface uintptr - GetPrivateData uintptr - GetParent uintptr - } -} - -type IDXGIAdapter struct { - Vtbl *struct { - _IUnknownVTbl - SetPrivateData uintptr - SetPrivateDataInterface uintptr - GetPrivateData uintptr - GetParent uintptr - EnumOutputs uintptr - GetDesc uintptr - CheckInterfaceSupport uintptr - GetDesc1 uintptr - } -} - -type IDXGIFactory struct { - Vtbl *struct { - _IUnknownVTbl - SetPrivateData uintptr - SetPrivateDataInterface uintptr - GetPrivateData uintptr - GetParent uintptr - EnumAdapters uintptr - MakeWindowAssociation uintptr - GetWindowAssociation uintptr - CreateSwapChain uintptr - CreateSoftwareAdapter uintptr - } -} - -type IDXGIDebug struct { - Vtbl *struct { - _IUnknownVTbl - ReportLiveObjects uintptr - } -} - -type IDXGIDevice struct { - Vtbl *struct { - _IUnknownVTbl - SetPrivateData uintptr - SetPrivateDataInterface uintptr - GetPrivateData uintptr - GetParent uintptr - GetAdapter uintptr - CreateSurface uintptr - QueryResourceResidency uintptr - SetGPUThreadPriority uintptr - GetGPUThreadPriority uintptr - } -} - -type IUnknown struct { - Vtbl *struct { - _IUnknownVTbl - } -} - -type _IUnknownVTbl struct { - QueryInterface uintptr - AddRef uintptr - Release uintptr -} - -type BUFFER_DESC struct { - ByteWidth uint32 - Usage uint32 - BindFlags uint32 - CPUAccessFlags uint32 - MiscFlags uint32 - StructureByteStride uint32 -} - -type GUID struct { - Data1 uint32 - Data2 uint16 - Data3 uint16 - Data4_0 uint8 - Data4_1 uint8 - Data4_2 uint8 - Data4_3 uint8 - Data4_4 uint8 - Data4_5 uint8 - Data4_6 uint8 - Data4_7 uint8 -} - -type VIEWPORT struct { - TopLeftX float32 - TopLeftY float32 - Width float32 - Height float32 - MinDepth float32 - MaxDepth float32 -} - -type SUBRESOURCE_DATA struct { - pSysMem *byte -} - -type BOX struct { - Left uint32 - Top uint32 - Front uint32 - Right uint32 - Bottom uint32 - Back uint32 -} - -type MAPPED_SUBRESOURCE struct { - PData uintptr - RowPitch uint32 - DepthPitch uint32 -} - -type ErrorCode struct { - Name string - Code uint32 -} - -type RASTERIZER_DESC struct { - FillMode uint32 - CullMode uint32 - FrontCounterClockwise uint32 - DepthBias int32 - DepthBiasClamp float32 - SlopeScaledDepthBias float32 - DepthClipEnable uint32 - ScissorEnable uint32 - MultisampleEnable uint32 - AntialiasedLineEnable uint32 -} - -var ( - IID_Texture2D = GUID{0x6f15aaf2, 0xd208, 0x4e89, 0x9a, 0xb4, 0x48, 0x95, 0x35, 0xd3, 0x4f, 0x9c} - IID_IDXGIDebug = GUID{0x119E7452, 0xDE9E, 0x40fe, 0x88, 0x06, 0x88, 0xF9, 0x0C, 0x12, 0xB4, 0x41} - IID_IDXGIDevice = GUID{0x54ec77fa, 0x1377, 0x44e6, 0x8c, 0x32, 0x88, 0xfd, 0x5f, 0x44, 0xc8, 0x4c} - IID_IDXGIFactory = GUID{0x7b7166ec, 0x21c7, 0x44ae, 0xb2, 0x1a, 0xc9, 0xae, 0x32, 0x1a, 0xe3, 0x69} - IID_ID3D11Debug = GUID{0x79cf2233, 0x7536, 0x4948, 0x9d, 0x36, 0x1e, 0x46, 0x92, 0xdc, 0x57, 0x60} - - DXGI_DEBUG_ALL = GUID{0xe48ae283, 0xda80, 0x490b, 0x87, 0xe6, 0x43, 0xe9, 0xa9, 0xcf, 0xda, 0x8} -) - -var ( - d3d11 = windows.NewLazySystemDLL("d3d11.dll") - - _D3D11CreateDevice = d3d11.NewProc("D3D11CreateDevice") - _D3D11CreateDeviceAndSwapChain = d3d11.NewProc("D3D11CreateDeviceAndSwapChain") - - dxgi = windows.NewLazySystemDLL("dxgi.dll") - - _DXGIGetDebugInterface1 = dxgi.NewProc("DXGIGetDebugInterface1") -) - -const ( - SDK_VERSION = 7 - DRIVER_TYPE_HARDWARE = 1 - - DXGI_FORMAT_UNKNOWN = 0 - DXGI_FORMAT_R16_FLOAT = 54 - DXGI_FORMAT_R32_FLOAT = 41 - DXGI_FORMAT_R32_TYPELESS = 39 - DXGI_FORMAT_R32G32_FLOAT = 16 - DXGI_FORMAT_R32G32B32_FLOAT = 6 - DXGI_FORMAT_R32G32B32A32_FLOAT = 2 - DXGI_FORMAT_R8G8B8A8_UNORM = 28 - DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29 - DXGI_FORMAT_R16_SINT = 59 - DXGI_FORMAT_R16G16_SINT = 38 - DXGI_FORMAT_R16_UINT = 57 - DXGI_FORMAT_D24_UNORM_S8_UINT = 45 - DXGI_FORMAT_R16G16_FLOAT = 34 - DXGI_FORMAT_R16G16B16A16_FLOAT = 10 - - DXGI_DEBUG_RLO_SUMMARY = 0x1 - DXGI_DEBUG_RLO_DETAIL = 0x2 - DXGI_DEBUG_RLO_IGNORE_INTERNAL = 0x4 - - FORMAT_SUPPORT_TEXTURE2D = 0x20 - FORMAT_SUPPORT_RENDER_TARGET = 0x4000 - - DXGI_USAGE_RENDER_TARGET_OUTPUT = 1 << (1 + 4) - - CPU_ACCESS_READ = 0x20000 - - MAP_READ = 1 - - DXGI_SWAP_EFFECT_DISCARD = 0 - - FEATURE_LEVEL_9_1 = 0x9100 - FEATURE_LEVEL_9_3 = 0x9300 - FEATURE_LEVEL_11_0 = 0xb000 - - USAGE_IMMUTABLE = 1 - USAGE_STAGING = 3 - - BIND_VERTEX_BUFFER = 0x1 - BIND_INDEX_BUFFER = 0x2 - BIND_CONSTANT_BUFFER = 0x4 - BIND_SHADER_RESOURCE = 0x8 - BIND_RENDER_TARGET = 0x20 - BIND_DEPTH_STENCIL = 0x40 - BIND_UNORDERED_ACCESS = 0x80 - - PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4 - PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5 - - FILTER_MIN_MAG_LINEAR_MIP_POINT = 0x14 - FILTER_MIN_MAG_MIP_LINEAR = 0x15 - FILTER_MIN_MAG_MIP_POINT = 0 - - TEXTURE_ADDRESS_MIRROR = 2 - TEXTURE_ADDRESS_CLAMP = 3 - TEXTURE_ADDRESS_WRAP = 1 - - SRV_DIMENSION_BUFFER = 1 - UAV_DIMENSION_BUFFER = 1 - SRV_DIMENSION_BUFFEREX = 11 - SRV_DIMENSION_TEXTURE2D = 4 - UAV_DIMENSION_TEXTURE2D = 4 - - BUFFER_UAV_FLAG_RAW = 0x1 - BUFFEREX_SRV_FLAG_RAW = 0x1 - - RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS = 0x20 - RESOURCE_MISC_GENERATE_MIPS = 0x1 - - CREATE_DEVICE_DEBUG = 0x2 - - FILL_SOLID = 3 - - CULL_NONE = 1 - - CLEAR_DEPTH = 0x1 - CLEAR_STENCIL = 0x2 - - DSV_DIMENSION_TEXTURE2D = 3 - - DEPTH_WRITE_MASK_ALL = 1 - - COMPARISON_GREATER = 5 - COMPARISON_GREATER_EQUAL = 7 - - BLEND_OP_ADD = 1 - BLEND_ONE = 2 - BLEND_INV_SRC_ALPHA = 6 - BLEND_ZERO = 1 - BLEND_DEST_COLOR = 9 - BLEND_DEST_ALPHA = 7 - - COLOR_WRITE_ENABLE_ALL = 1 | 2 | 4 | 8 - - DXGI_STATUS_OCCLUDED = 0x087A0001 - DXGI_ERROR_DEVICE_RESET = 0x887A0007 - DXGI_ERROR_DEVICE_REMOVED = 0x887A0005 - D3DDDIERR_DEVICEREMOVED = 1<<31 | 0x876<<16 | 2160 - - RLDO_SUMMARY = 1 - RLDO_DETAIL = 2 - RLDO_IGNORE_INTERNAL = 4 -) - -func CreateDevice(driverType uint32, flags uint32) (*Device, *DeviceContext, uint32, error) { - var ( - dev *Device - ctx *DeviceContext - featLvl uint32 - ) - r, _, _ := _D3D11CreateDevice.Call( - 0, // pAdapter - uintptr(driverType), // driverType - 0, // Software - uintptr(flags), // Flags - 0, // pFeatureLevels - 0, // FeatureLevels - SDK_VERSION, // SDKVersion - uintptr(unsafe.Pointer(&dev)), // ppDevice - uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel - uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext - ) - if r != 0 { - return nil, nil, 0, ErrorCode{Name: "D3D11CreateDevice", Code: uint32(r)} - } - return dev, ctx, featLvl, nil -} - -func CreateDeviceAndSwapChain(driverType uint32, flags uint32, swapDesc *DXGI_SWAP_CHAIN_DESC) (*Device, *DeviceContext, *IDXGISwapChain, uint32, error) { - var ( - dev *Device - ctx *DeviceContext - swchain *IDXGISwapChain - featLvl uint32 - ) - r, _, _ := _D3D11CreateDeviceAndSwapChain.Call( - 0, // pAdapter - uintptr(driverType), // driverType - 0, // Software - uintptr(flags), // Flags - 0, // pFeatureLevels - 0, // FeatureLevels - SDK_VERSION, // SDKVersion - uintptr(unsafe.Pointer(swapDesc)), // pSwapChainDesc - uintptr(unsafe.Pointer(&swchain)), // ppSwapChain - uintptr(unsafe.Pointer(&dev)), // ppDevice - uintptr(unsafe.Pointer(&featLvl)), // pFeatureLevel - uintptr(unsafe.Pointer(&ctx)), // ppImmediateContext - ) - if r != 0 { - return nil, nil, nil, 0, ErrorCode{Name: "D3D11CreateDeviceAndSwapChain", Code: uint32(r)} - } - return dev, ctx, swchain, featLvl, nil -} - -func DXGIGetDebugInterface1() (*IDXGIDebug, error) { - var dbg *IDXGIDebug - r, _, _ := _DXGIGetDebugInterface1.Call( - 0, // Flags - uintptr(unsafe.Pointer(&IID_IDXGIDebug)), - uintptr(unsafe.Pointer(&dbg)), - ) - if r != 0 { - return nil, ErrorCode{Name: "DXGIGetDebugInterface1", Code: uint32(r)} - } - return dbg, nil -} - -func ReportLiveObjects() error { - dxgi, err := DXGIGetDebugInterface1() - if err != nil { - return err - } - defer IUnknownRelease(unsafe.Pointer(dxgi), dxgi.Vtbl.Release) - dxgi.ReportLiveObjects(&DXGI_DEBUG_ALL, DXGI_DEBUG_RLO_DETAIL|DXGI_DEBUG_RLO_IGNORE_INTERNAL) - return nil -} - -func (d *IDXGIDebug) ReportLiveObjects(guid *GUID, flags uint32) { - syscall.Syscall6( - d.Vtbl.ReportLiveObjects, - 3, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(guid)), - uintptr(flags), - 0, - 0, - 0, - ) -} - -func (d *Device) CheckFormatSupport(format uint32) (uint32, error) { - var support uint32 - r, _, _ := syscall.Syscall( - d.Vtbl.CheckFormatSupport, - 3, - uintptr(unsafe.Pointer(d)), - uintptr(format), - uintptr(unsafe.Pointer(&support)), - ) - if r != 0 { - return 0, ErrorCode{Name: "DeviceCheckFormatSupport", Code: uint32(r)} - } - return support, nil -} - -func (d *Device) CreateBuffer(desc *BUFFER_DESC, data []byte) (*Buffer, error) { - var dataDesc *SUBRESOURCE_DATA - if len(data) > 0 { - dataDesc = &SUBRESOURCE_DATA{ - pSysMem: &data[0], - } - } - var buf *Buffer - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateBuffer, - 4, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(desc)), - uintptr(unsafe.Pointer(dataDesc)), - uintptr(unsafe.Pointer(&buf)), - 0, 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateBuffer", Code: uint32(r)} - } - return buf, nil -} - -func (d *Device) CreateDepthStencilViewTEX2D(res *Resource, desc *DEPTH_STENCIL_VIEW_DESC_TEX2D) (*DepthStencilView, error) { - var view *DepthStencilView - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateDepthStencilView, - 4, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(res)), - uintptr(unsafe.Pointer(desc)), - uintptr(unsafe.Pointer(&view)), - 0, 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateDepthStencilView", Code: uint32(r)} - } - return view, nil -} - -func (d *Device) CreatePixelShader(bytecode []byte) (*PixelShader, error) { - var shader *PixelShader - r, _, _ := syscall.Syscall6( - d.Vtbl.CreatePixelShader, - 5, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(&bytecode[0])), - uintptr(len(bytecode)), - 0, // pClassLinkage - uintptr(unsafe.Pointer(&shader)), - 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreatePixelShader", Code: uint32(r)} - } - return shader, nil -} - -func (d *Device) CreateVertexShader(bytecode []byte) (*VertexShader, error) { - var shader *VertexShader - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateVertexShader, - 5, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(&bytecode[0])), - uintptr(len(bytecode)), - 0, // pClassLinkage - uintptr(unsafe.Pointer(&shader)), - 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateVertexShader", Code: uint32(r)} - } - return shader, nil -} - -func (d *Device) CreateComputeShader(bytecode []byte) (*ComputeShader, error) { - var shader *ComputeShader - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateComputeShader, - 5, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(&bytecode[0])), - uintptr(len(bytecode)), - 0, // pClassLinkage - uintptr(unsafe.Pointer(&shader)), - 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateComputeShader", Code: uint32(r)} - } - return shader, nil -} - -func (d *Device) CreateShaderResourceView(res *Resource, desc unsafe.Pointer) (*ShaderResourceView, error) { - var resView *ShaderResourceView - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateShaderResourceView, - 4, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(res)), - uintptr(desc), - uintptr(unsafe.Pointer(&resView)), - 0, 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateShaderResourceView", Code: uint32(r)} - } - return resView, nil -} - -func (d *Device) CreateUnorderedAccessView(res *Resource, desc unsafe.Pointer) (*UnorderedAccessView, error) { - var uaView *UnorderedAccessView - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateUnorderedAccessView, - 4, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(res)), - uintptr(desc), - uintptr(unsafe.Pointer(&uaView)), - 0, 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateUnorderedAccessView", Code: uint32(r)} - } - return uaView, nil -} - -func (d *Device) CreateRasterizerState(desc *RASTERIZER_DESC) (*RasterizerState, error) { - var state *RasterizerState - r, _, _ := syscall.Syscall( - d.Vtbl.CreateRasterizerState, - 3, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(desc)), - uintptr(unsafe.Pointer(&state)), - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateRasterizerState", Code: uint32(r)} - } - return state, nil -} - -func (d *Device) CreateInputLayout(descs []INPUT_ELEMENT_DESC, bytecode []byte) (*InputLayout, error) { - var pdesc *INPUT_ELEMENT_DESC - if len(descs) > 0 { - pdesc = &descs[0] - } - var layout *InputLayout - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateInputLayout, - 6, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(pdesc)), - uintptr(len(descs)), - uintptr(unsafe.Pointer(&bytecode[0])), - uintptr(len(bytecode)), - uintptr(unsafe.Pointer(&layout)), - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateInputLayout", Code: uint32(r)} - } - return layout, nil -} - -func (d *Device) CreateSamplerState(desc *SAMPLER_DESC) (*SamplerState, error) { - var sampler *SamplerState - r, _, _ := syscall.Syscall( - d.Vtbl.CreateSamplerState, - 3, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(desc)), - uintptr(unsafe.Pointer(&sampler)), - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateSamplerState", Code: uint32(r)} - } - return sampler, nil -} - -func (d *Device) CreateTexture2D(desc *TEXTURE2D_DESC) (*Texture2D, error) { - var tex *Texture2D - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateTexture2D, - 4, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(desc)), - 0, // pInitialData - uintptr(unsafe.Pointer(&tex)), - 0, 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "CreateTexture2D", Code: uint32(r)} - } - return tex, nil -} - -func (d *Device) CreateRenderTargetView(res *Resource) (*RenderTargetView, error) { - var target *RenderTargetView - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateRenderTargetView, - 4, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(res)), - 0, // pDesc - uintptr(unsafe.Pointer(&target)), - 0, 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateRenderTargetView", Code: uint32(r)} - } - return target, nil -} - -func (d *Device) CreateBlendState(desc *BLEND_DESC) (*BlendState, error) { - var state *BlendState - r, _, _ := syscall.Syscall( - d.Vtbl.CreateBlendState, - 3, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(desc)), - uintptr(unsafe.Pointer(&state)), - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateBlendState", Code: uint32(r)} - } - return state, nil -} - -func (d *Device) CreateDepthStencilState(desc *DEPTH_STENCIL_DESC) (*DepthStencilState, error) { - var state *DepthStencilState - r, _, _ := syscall.Syscall( - d.Vtbl.CreateDepthStencilState, - 3, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(desc)), - uintptr(unsafe.Pointer(&state)), - ) - if r != 0 { - return nil, ErrorCode{Name: "DeviceCreateDepthStencilState", Code: uint32(r)} - } - return state, nil -} - -func (d *Device) GetFeatureLevel() int { - lvl, _, _ := syscall.Syscall( - d.Vtbl.GetFeatureLevel, - 1, - uintptr(unsafe.Pointer(d)), - 0, 0, - ) - return int(lvl) -} - -func (d *Device) GetImmediateContext() *DeviceContext { - var ctx *DeviceContext - syscall.Syscall( - d.Vtbl.GetImmediateContext, - 2, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(&ctx)), - 0, - ) - return ctx -} - -func (d *Device) ReportLiveDeviceObjects() error { - intf, err := IUnknownQueryInterface(unsafe.Pointer(d), d.Vtbl.QueryInterface, &IID_ID3D11Debug) - if err != nil { - return fmt.Errorf("ReportLiveObjects: failed to query ID3D11Debug interface: %v", err) - } - defer IUnknownRelease(unsafe.Pointer(intf), intf.Vtbl.Release) - dbg := (*Debug)(unsafe.Pointer(intf)) - dbg.ReportLiveDeviceObjects(RLDO_DETAIL | RLDO_IGNORE_INTERNAL) - return nil -} - -func (d *Debug) ReportLiveDeviceObjects(flags uint32) { - syscall.Syscall( - d.Vtbl.ReportLiveDeviceObjects, - 2, - uintptr(unsafe.Pointer(d)), - uintptr(flags), - 0, - ) -} - -func (s *IDXGISwapChain) GetDesc() (DXGI_SWAP_CHAIN_DESC, error) { - var desc DXGI_SWAP_CHAIN_DESC - r, _, _ := syscall.Syscall( - s.Vtbl.GetDesc, - 2, - uintptr(unsafe.Pointer(s)), - uintptr(unsafe.Pointer(&desc)), - 0, - ) - if r != 0 { - return DXGI_SWAP_CHAIN_DESC{}, ErrorCode{Name: "IDXGISwapChainGetDesc", Code: uint32(r)} - } - return desc, nil -} - -func (s *IDXGISwapChain) ResizeBuffers(buffers, width, height, newFormat, flags uint32) error { - r, _, _ := syscall.Syscall6( - s.Vtbl.ResizeBuffers, - 6, - uintptr(unsafe.Pointer(s)), - uintptr(buffers), - uintptr(width), - uintptr(height), - uintptr(newFormat), - uintptr(flags), - ) - if r != 0 { - return ErrorCode{Name: "IDXGISwapChainResizeBuffers", Code: uint32(r)} - } - return nil -} - -func (s *IDXGISwapChain) Present(SyncInterval int, Flags uint32) error { - r, _, _ := syscall.Syscall( - s.Vtbl.Present, - 3, - uintptr(unsafe.Pointer(s)), - uintptr(SyncInterval), - uintptr(Flags), - ) - if r != 0 { - return ErrorCode{Name: "IDXGISwapChainPresent", Code: uint32(r)} - } - return nil -} - -func (s *IDXGISwapChain) GetBuffer(index int, riid *GUID) (*IUnknown, error) { - var buf *IUnknown - r, _, _ := syscall.Syscall6( - s.Vtbl.GetBuffer, - 4, - uintptr(unsafe.Pointer(s)), - uintptr(index), - uintptr(unsafe.Pointer(riid)), - uintptr(unsafe.Pointer(&buf)), - 0, - 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "IDXGISwapChainGetBuffer", Code: uint32(r)} - } - return buf, nil -} - -func (c *DeviceContext) GenerateMips(res *ShaderResourceView) { - syscall.Syscall( - c.Vtbl.GenerateMips, - 2, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(res)), - 0, - ) -} - -func (c *DeviceContext) Unmap(resource *Resource, subResource uint32) { - syscall.Syscall( - c.Vtbl.Unmap, - 3, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(resource)), - uintptr(subResource), - ) -} - -func (c *DeviceContext) Map(resource *Resource, subResource, mapType, mapFlags uint32) (MAPPED_SUBRESOURCE, error) { - var resMap MAPPED_SUBRESOURCE - r, _, _ := syscall.Syscall6( - c.Vtbl.Map, - 6, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(resource)), - uintptr(subResource), - uintptr(mapType), - uintptr(mapFlags), - uintptr(unsafe.Pointer(&resMap)), - ) - if r != 0 { - return resMap, ErrorCode{Name: "DeviceContextMap", Code: uint32(r)} - } - return resMap, nil -} - -func (c *DeviceContext) CopySubresourceRegion(dst *Resource, dstSubresource, dstX, dstY, dstZ uint32, src *Resource, srcSubresource uint32, srcBox *BOX) { - syscall.Syscall9( - c.Vtbl.CopySubresourceRegion, - 9, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(dst)), - uintptr(dstSubresource), - uintptr(dstX), - uintptr(dstY), - uintptr(dstZ), - uintptr(unsafe.Pointer(src)), - uintptr(srcSubresource), - uintptr(unsafe.Pointer(srcBox)), - ) -} - -func (c *DeviceContext) ClearDepthStencilView(target *DepthStencilView, flags uint32, depth float32, stencil uint8) { - syscall.Syscall6( - c.Vtbl.ClearDepthStencilView, - 5, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(target)), - uintptr(flags), - uintptr(math.Float32bits(depth)), - uintptr(stencil), - 0, - ) -} - -func (c *DeviceContext) ClearRenderTargetView(target *RenderTargetView, color *[4]float32) { - syscall.Syscall( - c.Vtbl.ClearRenderTargetView, - 3, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(target)), - uintptr(unsafe.Pointer(color)), - ) -} - -func (c *DeviceContext) CSSetShaderResources(startSlot uint32, s *ShaderResourceView) { - syscall.Syscall6( - c.Vtbl.CSSetShaderResources, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(startSlot), - 1, // NumViews - uintptr(unsafe.Pointer(&s)), - 0, 0, - ) -} - -func (c *DeviceContext) CSSetUnorderedAccessViews(startSlot uint32, v *UnorderedAccessView) { - syscall.Syscall6( - c.Vtbl.CSSetUnorderedAccessViews, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(startSlot), - 1, // NumViews - uintptr(unsafe.Pointer(&v)), - 0, 0, - ) -} - -func (c *DeviceContext) CSSetShader(s *ComputeShader) { - syscall.Syscall6( - c.Vtbl.CSSetShader, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(s)), - 0, // ppClassInstances - 0, // NumClassInstances - 0, 0, - ) -} - -func (c *DeviceContext) RSSetViewports(viewport *VIEWPORT) { - syscall.Syscall( - c.Vtbl.RSSetViewports, - 3, - uintptr(unsafe.Pointer(c)), - 1, // NumViewports - uintptr(unsafe.Pointer(viewport)), - ) -} - -func (c *DeviceContext) VSSetShader(s *VertexShader) { - syscall.Syscall6( - c.Vtbl.VSSetShader, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(s)), - 0, // ppClassInstances - 0, // NumClassInstances - 0, 0, - ) -} - -func (c *DeviceContext) VSSetConstantBuffers(b *Buffer) { - syscall.Syscall6( - c.Vtbl.VSSetConstantBuffers, - 4, - uintptr(unsafe.Pointer(c)), - 0, // StartSlot - 1, // NumBuffers - uintptr(unsafe.Pointer(&b)), - 0, 0, - ) -} - -func (c *DeviceContext) PSSetConstantBuffers(b *Buffer) { - syscall.Syscall6( - c.Vtbl.PSSetConstantBuffers, - 4, - uintptr(unsafe.Pointer(c)), - 0, // StartSlot - 1, // NumBuffers - uintptr(unsafe.Pointer(&b)), - 0, 0, - ) -} - -func (c *DeviceContext) PSSetShaderResources(startSlot uint32, s *ShaderResourceView) { - syscall.Syscall6( - c.Vtbl.PSSetShaderResources, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(startSlot), - 1, // NumViews - uintptr(unsafe.Pointer(&s)), - 0, 0, - ) -} - -func (c *DeviceContext) PSSetSamplers(startSlot uint32, s *SamplerState) { - syscall.Syscall6( - c.Vtbl.PSSetSamplers, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(startSlot), - 1, // NumSamplers - uintptr(unsafe.Pointer(&s)), - 0, 0, - ) -} - -func (c *DeviceContext) PSSetShader(s *PixelShader) { - syscall.Syscall6( - c.Vtbl.PSSetShader, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(s)), - 0, // ppClassInstances - 0, // NumClassInstances - 0, 0, - ) -} - -func (c *DeviceContext) UpdateSubresource(res *Resource, dstBox *BOX, rowPitch, depthPitch uint32, data []byte) { - syscall.Syscall9( - c.Vtbl.UpdateSubresource, - 7, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(res)), - 0, // DstSubresource - uintptr(unsafe.Pointer(dstBox)), - uintptr(unsafe.Pointer(&data[0])), - uintptr(rowPitch), - uintptr(depthPitch), - 0, 0, - ) -} - -func (c *DeviceContext) RSSetState(state *RasterizerState) { - syscall.Syscall( - c.Vtbl.RSSetState, - 2, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(state)), - 0, - ) -} - -func (c *DeviceContext) IASetInputLayout(layout *InputLayout) { - syscall.Syscall( - c.Vtbl.IASetInputLayout, - 2, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(layout)), - 0, - ) -} - -func (c *DeviceContext) IASetIndexBuffer(buf *Buffer, format, offset uint32) { - syscall.Syscall6( - c.Vtbl.IASetIndexBuffer, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(buf)), - uintptr(format), - uintptr(offset), - 0, 0, - ) -} - -func (c *DeviceContext) IASetVertexBuffers(buf *Buffer, stride, offset uint32) { - syscall.Syscall6( - c.Vtbl.IASetVertexBuffers, - 6, - uintptr(unsafe.Pointer(c)), - 0, // StartSlot - 1, // NumBuffers, - uintptr(unsafe.Pointer(&buf)), - uintptr(unsafe.Pointer(&stride)), - uintptr(unsafe.Pointer(&offset)), - ) -} - -func (c *DeviceContext) IASetPrimitiveTopology(mode uint32) { - syscall.Syscall( - c.Vtbl.IASetPrimitiveTopology, - 2, - uintptr(unsafe.Pointer(c)), - uintptr(mode), - 0, - ) -} - -func (c *DeviceContext) OMGetRenderTargets() (*RenderTargetView, *DepthStencilView) { - var ( - target *RenderTargetView - depthStencilView *DepthStencilView - ) - syscall.Syscall6( - c.Vtbl.OMGetRenderTargets, - 4, - uintptr(unsafe.Pointer(c)), - 1, // NumViews - uintptr(unsafe.Pointer(&target)), - uintptr(unsafe.Pointer(&depthStencilView)), - 0, 0, - ) - return target, depthStencilView -} - -func (c *DeviceContext) OMSetRenderTargets(target *RenderTargetView, depthStencil *DepthStencilView) { - syscall.Syscall6( - c.Vtbl.OMSetRenderTargets, - 4, - uintptr(unsafe.Pointer(c)), - 1, // NumViews - uintptr(unsafe.Pointer(&target)), - uintptr(unsafe.Pointer(depthStencil)), - 0, 0, - ) -} - -func (c *DeviceContext) Draw(count, start uint32) { - syscall.Syscall( - c.Vtbl.Draw, - 3, - uintptr(unsafe.Pointer(c)), - uintptr(count), - uintptr(start), - ) -} - -func (c *DeviceContext) DrawIndexed(count, start uint32, base int32) { - syscall.Syscall6( - c.Vtbl.DrawIndexed, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(count), - uintptr(start), - uintptr(base), - 0, 0, - ) -} - -func (c *DeviceContext) Dispatch(x, y, z uint32) { - syscall.Syscall6( - c.Vtbl.Dispatch, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(x), - uintptr(y), - uintptr(z), - 0, 0, - ) -} - -func (c *DeviceContext) OMSetBlendState(state *BlendState, factor *f32color.RGBA, sampleMask uint32) { - syscall.Syscall6( - c.Vtbl.OMSetBlendState, - 4, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(state)), - uintptr(unsafe.Pointer(factor)), - uintptr(sampleMask), - 0, 0, - ) -} - -func (c *DeviceContext) OMSetDepthStencilState(state *DepthStencilState, stencilRef uint32) { - syscall.Syscall( - c.Vtbl.OMSetDepthStencilState, - 3, - uintptr(unsafe.Pointer(c)), - uintptr(unsafe.Pointer(state)), - uintptr(stencilRef), - ) -} - -func (d *IDXGIObject) GetParent(guid *GUID) (*IDXGIObject, error) { - var parent *IDXGIObject - r, _, _ := syscall.Syscall( - d.Vtbl.GetParent, - 3, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(guid)), - uintptr(unsafe.Pointer(&parent)), - ) - if r != 0 { - return nil, ErrorCode{Name: "IDXGIObjectGetParent", Code: uint32(r)} - } - return parent, nil -} - -func (d *IDXGIFactory) CreateSwapChain(device *IUnknown, desc *DXGI_SWAP_CHAIN_DESC) (*IDXGISwapChain, error) { - var swchain *IDXGISwapChain - r, _, _ := syscall.Syscall6( - d.Vtbl.CreateSwapChain, - 4, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(device)), - uintptr(unsafe.Pointer(desc)), - uintptr(unsafe.Pointer(&swchain)), - 0, 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "IDXGIFactory", Code: uint32(r)} - } - return swchain, nil -} - -func (d *IDXGIDevice) GetAdapter() (*IDXGIAdapter, error) { - var adapter *IDXGIAdapter - r, _, _ := syscall.Syscall( - d.Vtbl.GetAdapter, - 2, - uintptr(unsafe.Pointer(d)), - uintptr(unsafe.Pointer(&adapter)), - 0, - ) - if r != 0 { - return nil, ErrorCode{Name: "IDXGIDeviceGetAdapter", Code: uint32(r)} - } - return adapter, nil -} - -func IUnknownQueryInterface(obj unsafe.Pointer, queryInterfaceMethod uintptr, guid *GUID) (*IUnknown, error) { - var ref *IUnknown - r, _, _ := syscall.Syscall( - queryInterfaceMethod, - 3, - uintptr(obj), - uintptr(unsafe.Pointer(guid)), - uintptr(unsafe.Pointer(&ref)), - ) - if r != 0 { - return nil, ErrorCode{Name: "IUnknownQueryInterface", Code: uint32(r)} - } - return ref, nil -} - -func IUnknownAddRef(obj unsafe.Pointer, addRefMethod uintptr) { - syscall.Syscall( - addRefMethod, - 1, - uintptr(obj), - 0, - 0, - ) -} - -func IUnknownRelease(obj unsafe.Pointer, releaseMethod uintptr) { - syscall.Syscall( - releaseMethod, - 1, - uintptr(obj), - 0, - 0, - ) -} - -func (e ErrorCode) Error() string { - return fmt.Sprintf("%s: %#x", e.Name, e.Code) -} - -func CreateSwapChain(dev *Device, hwnd windows.Handle) (*IDXGISwapChain, error) { - dxgiDev, err := IUnknownQueryInterface(unsafe.Pointer(dev), dev.Vtbl.QueryInterface, &IID_IDXGIDevice) - if err != nil { - return nil, fmt.Errorf("NewContext: %v", err) - } - adapter, err := (*IDXGIDevice)(unsafe.Pointer(dxgiDev)).GetAdapter() - IUnknownRelease(unsafe.Pointer(dxgiDev), dxgiDev.Vtbl.Release) - if err != nil { - return nil, fmt.Errorf("NewContext: %v", err) - } - dxgiFactory, err := (*IDXGIObject)(unsafe.Pointer(adapter)).GetParent(&IID_IDXGIFactory) - IUnknownRelease(unsafe.Pointer(adapter), adapter.Vtbl.Release) - if err != nil { - return nil, fmt.Errorf("NewContext: %v", err) - } - swchain, err := (*IDXGIFactory)(unsafe.Pointer(dxgiFactory)).CreateSwapChain( - (*IUnknown)(unsafe.Pointer(dev)), - &DXGI_SWAP_CHAIN_DESC{ - BufferDesc: DXGI_MODE_DESC{ - Format: DXGI_FORMAT_R8G8B8A8_UNORM_SRGB, - }, - SampleDesc: DXGI_SAMPLE_DESC{ - Count: 1, - }, - BufferUsage: DXGI_USAGE_RENDER_TARGET_OUTPUT, - BufferCount: 1, - OutputWindow: hwnd, - Windowed: 1, - SwapEffect: DXGI_SWAP_EFFECT_DISCARD, - }, - ) - IUnknownRelease(unsafe.Pointer(dxgiFactory), dxgiFactory.Vtbl.Release) - if err != nil { - return nil, fmt.Errorf("NewContext: %v", err) - } - return swchain, nil -} - -func CreateDepthView(d *Device, width, height, depthBits int) (*DepthStencilView, error) { - depthTex, err := d.CreateTexture2D(&TEXTURE2D_DESC{ - Width: uint32(width), - Height: uint32(height), - MipLevels: 1, - ArraySize: 1, - Format: DXGI_FORMAT_D24_UNORM_S8_UINT, - SampleDesc: DXGI_SAMPLE_DESC{ - Count: 1, - Quality: 0, - }, - BindFlags: BIND_DEPTH_STENCIL, - }) - if err != nil { - return nil, err - } - depthView, err := d.CreateDepthStencilViewTEX2D( - (*Resource)(unsafe.Pointer(depthTex)), - &DEPTH_STENCIL_VIEW_DESC_TEX2D{ - Format: DXGI_FORMAT_D24_UNORM_S8_UINT, - ViewDimension: DSV_DIMENSION_TEXTURE2D, - }, - ) - IUnknownRelease(unsafe.Pointer(depthTex), depthTex.Vtbl.Release) - return depthView, err -} diff --git a/gio/internal/debug/debug.go b/gio/internal/debug/debug.go deleted file mode 100644 index f693933..0000000 --- a/gio/internal/debug/debug.go +++ /dev/null @@ -1,57 +0,0 @@ -// Package debug provides general debug feature management for Gio, including -// the ability to toggle debug features using the GIODEBUG environment variable. -package debug - -import ( - "fmt" - "os" - "strings" - "sync" - "sync/atomic" -) - -const ( - debugVariable = "GIODEBUG" - textSubsystem = "text" - silentFeature = "silent" -) - -// Text controls whether the text subsystem has debug logging enabled. -var Text atomic.Bool - -var parseOnce sync.Once - -// Parse processes the current value of GIODEBUG. If it is unset, it does nothing. -// Otherwise it process its value, printing usage info the stderr if the value is -// not understood. Parse will be automatically invoked when the first application -// window is created, allowing applications to manipulate GIODEBUG programmatically -// before it is parsed. -func Parse() { - parseOnce.Do(func() { - val, ok := os.LookupEnv(debugVariable) - if !ok { - return - } - print := false - silent := false - for _, part := range strings.Split(val, ",") { - switch part { - case textSubsystem: - Text.Store(true) - case silentFeature: - silent = true - default: - print = true - } - } - if print && !silent { - fmt.Fprintf(os.Stderr, - `Usage of %s: - A comma-delimited list of debug subsystems to enable. Currently recognized systems: - - - %s: text debug info including system font resolution - - %s: silence this usage message even if GIODEBUG contains invalid content -`, debugVariable, textSubsystem, silentFeature) - } - }) -} diff --git a/gio/internal/egl/egl.go b/gio/internal/egl/egl.go deleted file mode 100644 index 48d08fe..0000000 --- a/gio/internal/egl/egl.go +++ /dev/null @@ -1,247 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build linux || windows || freebsd || openbsd -// +build linux windows freebsd openbsd - -package egl - -import ( - "errors" - "fmt" - "runtime" - "slices" - "strings" - - "github.com/p9c/p9/pkg/gel/gio/gpu" -) - -type Context struct { - disp _EGLDisplay - eglCtx *eglContext - eglSurf _EGLSurface -} - -type eglContext struct { - config _EGLConfig - ctx _EGLContext - visualID int - srgb bool - surfaceless bool -} - -var ( - nilEGLDisplay _EGLDisplay - nilEGLSurface _EGLSurface - nilEGLContext _EGLContext - nilEGLConfig _EGLConfig - EGL_DEFAULT_DISPLAY NativeDisplayType -) - -const ( - _EGL_ALPHA_SIZE = 0x3021 - _EGL_BLUE_SIZE = 0x3022 - _EGL_CONFIG_CAVEAT = 0x3027 - _EGL_CONTEXT_CLIENT_VERSION = 0x3098 - _EGL_DEPTH_SIZE = 0x3025 - _EGL_GL_COLORSPACE_KHR = 0x309d - _EGL_GL_COLORSPACE_SRGB_KHR = 0x3089 - _EGL_GREEN_SIZE = 0x3023 - _EGL_EXTENSIONS = 0x3055 - _EGL_NATIVE_VISUAL_ID = 0x302e - _EGL_NONE = 0x3038 - _EGL_OPENGL_ES2_BIT = 0x4 - _EGL_RED_SIZE = 0x3024 - _EGL_RENDERABLE_TYPE = 0x3040 - _EGL_SURFACE_TYPE = 0x3033 - _EGL_WINDOW_BIT = 0x4 -) - -func (c *Context) Release() { - c.ReleaseSurface() - if c.eglCtx != nil { - eglDestroyContext(c.disp, c.eglCtx.ctx) - c.eglCtx = nil - } - eglTerminate(c.disp) - c.disp = nilEGLDisplay -} - -func (c *Context) Present() error { - if !eglSwapBuffers(c.disp, c.eglSurf) { - return fmt.Errorf("eglSwapBuffers failed (%x)", eglGetError()) - } - return nil -} - -func NewContext(disp NativeDisplayType) (*Context, error) { - if err := loadEGL(); err != nil { - return nil, err - } - eglDisp := eglGetDisplay(disp) - // eglGetDisplay can return EGL_NO_DISPLAY yet no error - // (EGL_SUCCESS), in which case a default EGL display might be - // available. - if eglDisp == nilEGLDisplay { - eglDisp = eglGetDisplay(EGL_DEFAULT_DISPLAY) - } - if eglDisp == nilEGLDisplay { - return nil, fmt.Errorf("eglGetDisplay failed: 0x%x", eglGetError()) - } - eglCtx, err := createContext(eglDisp) - if err != nil { - return nil, err - } - c := &Context{ - disp: eglDisp, - eglCtx: eglCtx, - } - return c, nil -} - -func (c *Context) RenderTarget() (gpu.RenderTarget, error) { - return gpu.OpenGLRenderTarget{}, nil -} - -func (c *Context) API() gpu.API { - return gpu.OpenGL{} -} - -func (c *Context) ReleaseSurface() { - if c.eglSurf == nilEGLSurface { - return - } - // Make sure any in-flight GL commands are complete. - eglWaitClient() - c.ReleaseCurrent() - eglDestroySurface(c.disp, c.eglSurf) - c.eglSurf = nilEGLSurface -} - -func (c *Context) VisualID() int { - return c.eglCtx.visualID -} - -func (c *Context) CreateSurface(win NativeWindowType) error { - eglSurf, err := createSurface(c.disp, c.eglCtx, win) - c.eglSurf = eglSurf - return err -} - -func (c *Context) ReleaseCurrent() { - if c.disp != nilEGLDisplay { - eglMakeCurrent(c.disp, nilEGLSurface, nilEGLSurface, nilEGLContext) - } -} - -func (c *Context) MakeCurrent() error { - // OpenGL contexts are implicit and thread-local. Lock the OS thread. - runtime.LockOSThread() - - if c.eglSurf == nilEGLSurface && !c.eglCtx.surfaceless { - return errors.New("no surface created yet EGL_KHR_surfaceless_context is not supported") - } - if !eglMakeCurrent(c.disp, c.eglSurf, c.eglSurf, c.eglCtx.ctx) { - return fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError()) - } - return nil -} - -func (c *Context) EnableVSync(enable bool) { - if enable { - eglSwapInterval(c.disp, 1) - } else { - eglSwapInterval(c.disp, 0) - } -} - -func hasExtension(exts []string, ext string) bool { - return slices.Contains(exts, ext) -} - -func createContext(disp _EGLDisplay) (*eglContext, error) { - major, minor, ret := eglInitialize(disp) - if !ret { - return nil, fmt.Errorf("eglInitialize failed: 0x%x", eglGetError()) - } - // sRGB framebuffer support on EGL 1.5 or if EGL_KHR_gl_colorspace is supported. - exts := strings.Split(eglQueryString(disp, _EGL_EXTENSIONS), " ") - srgb := major > 1 || minor >= 5 || hasExtension(exts, "EGL_KHR_gl_colorspace") - attribs := []_EGLint{ - _EGL_RENDERABLE_TYPE, _EGL_OPENGL_ES2_BIT, - _EGL_SURFACE_TYPE, _EGL_WINDOW_BIT, - _EGL_BLUE_SIZE, 8, - _EGL_GREEN_SIZE, 8, - _EGL_RED_SIZE, 8, - _EGL_CONFIG_CAVEAT, _EGL_NONE, - } - if srgb { - if runtime.GOOS == "linux" || runtime.GOOS == "android" { - // Some Mesa drivers crash if an sRGB framebuffer is requested without alpha. - // https://bugs.freedesktop.org/show_bug.cgi?id=107782. - // - // Also, some Android devices (Samsung S9) need alpha for sRGB to work. - attribs = append(attribs, _EGL_ALPHA_SIZE, 8) - } - } - attribs = append(attribs, _EGL_NONE) - eglCfg, ret := eglChooseConfig(disp, attribs) - if !ret { - return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", eglGetError()) - } - if eglCfg == nilEGLConfig { - supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context") - if !supportsNoCfg { - return nil, errors.New("eglChooseConfig returned no configs") - } - } - var visID _EGLint - if eglCfg != nilEGLConfig { - var ok bool - visID, ok = eglGetConfigAttrib(disp, eglCfg, _EGL_NATIVE_VISUAL_ID) - if !ok { - return nil, errors.New("newContext: eglGetConfigAttrib for _EGL_NATIVE_VISUAL_ID failed") - } - } - ctxAttribs := []_EGLint{ - _EGL_CONTEXT_CLIENT_VERSION, 3, - _EGL_NONE, - } - eglCtx := eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs) - if eglCtx == nilEGLContext { - // Fall back to OpenGL ES 2 and rely on extensions. - ctxAttribs := []_EGLint{ - _EGL_CONTEXT_CLIENT_VERSION, 2, - _EGL_NONE, - } - eglCtx = eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs) - if eglCtx == nilEGLContext { - return nil, fmt.Errorf("eglCreateContext failed: 0x%x", eglGetError()) - } - } - return &eglContext{ - config: _EGLConfig(eglCfg), - ctx: _EGLContext(eglCtx), - visualID: int(visID), - srgb: srgb, - surfaceless: hasExtension(exts, "EGL_KHR_surfaceless_context"), - }, nil -} - -func createSurface(disp _EGLDisplay, eglCtx *eglContext, win NativeWindowType) (_EGLSurface, error) { - var surfAttribs []_EGLint - if eglCtx.srgb { - surfAttribs = append(surfAttribs, _EGL_GL_COLORSPACE_KHR, _EGL_GL_COLORSPACE_SRGB_KHR) - } - surfAttribs = append(surfAttribs, _EGL_NONE) - eglSurf := eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) - if eglSurf == nilEGLSurface && eglCtx.srgb { - // Try again without sRGB. - eglCtx.srgb = false - surfAttribs = []_EGLint{_EGL_NONE} - eglSurf = eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs) - } - if eglSurf == nilEGLSurface { - return nilEGLSurface, fmt.Errorf("newContext: eglCreateWindowSurface failed 0x%x (sRGB=%v)", eglGetError(), eglCtx.srgb) - } - return eglSurf, nil -} diff --git a/gio/internal/egl/egl_unix.go b/gio/internal/egl/egl_unix.go deleted file mode 100644 index bd3efa5..0000000 --- a/gio/internal/egl/egl_unix.go +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build linux || freebsd || openbsd -// +build linux freebsd openbsd - -package egl - -/* -#cgo linux,!android pkg-config: egl -#cgo freebsd openbsd android LDFLAGS: -lEGL -#cgo freebsd CFLAGS: -I/usr/local/include -#cgo freebsd LDFLAGS: -L/usr/local/lib -#cgo openbsd CFLAGS: -I/usr/X11R6/include -#cgo openbsd LDFLAGS: -L/usr/X11R6/lib -#cgo CFLAGS: -DEGL_NO_X11 - -#include -#include -*/ -import "C" - -type ( - _EGLint = C.EGLint - _EGLDisplay = C.EGLDisplay - _EGLConfig = C.EGLConfig - _EGLContext = C.EGLContext - _EGLSurface = C.EGLSurface - NativeDisplayType = C.EGLNativeDisplayType - NativeWindowType = C.EGLNativeWindowType -) - -func loadEGL() error { - return nil -} - -func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { - var cfg C.EGLConfig - var ncfg C.EGLint - if C.eglChooseConfig(disp, &attribs[0], &cfg, 1, &ncfg) != C.EGL_TRUE { - return nilEGLConfig, false - } - return _EGLConfig(cfg), true -} - -func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext { - ctx := C.eglCreateContext(disp, cfg, shareCtx, &attribs[0]) - return _EGLContext(ctx) -} - -func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { - return C.eglDestroySurface(disp, surf) == C.EGL_TRUE -} - -func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { - return C.eglDestroyContext(disp, ctx) == C.EGL_TRUE -} - -func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) { - var val _EGLint - ret := C.eglGetConfigAttrib(disp, cfg, attr, &val) - return val, ret == C.EGL_TRUE -} - -func eglGetError() _EGLint { - return C.eglGetError() -} - -func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { - var maj, min _EGLint - ret := C.eglInitialize(disp, &maj, &min) - return maj, min, ret == C.EGL_TRUE -} - -func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool { - return C.eglMakeCurrent(disp, draw, read, ctx) == C.EGL_TRUE -} - -func eglReleaseThread() bool { - return C.eglReleaseThread() == C.EGL_TRUE -} - -func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { - return C.eglSwapBuffers(disp, surf) == C.EGL_TRUE -} - -func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { - return C.eglSwapInterval(disp, interval) == C.EGL_TRUE -} - -func eglTerminate(disp _EGLDisplay) bool { - return C.eglTerminate(disp) == C.EGL_TRUE -} - -func eglQueryString(disp _EGLDisplay, name _EGLint) string { - return C.GoString(C.eglQueryString(disp, name)) -} - -func eglGetDisplay(disp NativeDisplayType) _EGLDisplay { - return C.eglGetDisplay(disp) -} - -func eglCreateWindowSurface(disp _EGLDisplay, conf _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface { - eglSurf := C.eglCreateWindowSurface(disp, conf, win, &attribs[0]) - return eglSurf -} - -func eglWaitClient() bool { - return C.eglWaitClient() == C.EGL_TRUE -} diff --git a/gio/internal/egl/egl_windows.go b/gio/internal/egl/egl_windows.go deleted file mode 100644 index f3cb529..0000000 --- a/gio/internal/egl/egl_windows.go +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package egl - -import ( - "fmt" - "runtime" - "sync" - "unsafe" - - syscall "golang.org/x/sys/windows" -) - -type ( - _EGLint int32 - _EGLDisplay uintptr - _EGLConfig uintptr - _EGLContext uintptr - _EGLSurface uintptr - NativeDisplayType uintptr - NativeWindowType uintptr -) - -var ( - libEGL = syscall.DLL{} - _eglChooseConfig *syscall.Proc - _eglCreateContext *syscall.Proc - _eglCreateWindowSurface *syscall.Proc - _eglDestroyContext *syscall.Proc - _eglDestroySurface *syscall.Proc - _eglGetConfigAttrib *syscall.Proc - _eglGetDisplay *syscall.Proc - _eglGetError *syscall.Proc - _eglInitialize *syscall.Proc - _eglMakeCurrent *syscall.Proc - _eglReleaseThread *syscall.Proc - _eglSwapInterval *syscall.Proc - _eglSwapBuffers *syscall.Proc - _eglTerminate *syscall.Proc - _eglQueryString *syscall.Proc - _eglWaitClient *syscall.Proc -) - -var loadOnce sync.Once - -func loadEGL() error { - var err error - loadOnce.Do(func() { - err = loadDLLs() - }) - return err -} - -func loadDLLs() error { - if err := loadDLL(&libEGL, "libEGL.dll"); err != nil { - return err - } - - procs := map[string]**syscall.Proc{ - "eglChooseConfig": &_eglChooseConfig, - "eglCreateContext": &_eglCreateContext, - "eglCreateWindowSurface": &_eglCreateWindowSurface, - "eglDestroyContext": &_eglDestroyContext, - "eglDestroySurface": &_eglDestroySurface, - "eglGetConfigAttrib": &_eglGetConfigAttrib, - "eglGetDisplay": &_eglGetDisplay, - "eglGetError": &_eglGetError, - "eglInitialize": &_eglInitialize, - "eglMakeCurrent": &_eglMakeCurrent, - "eglReleaseThread": &_eglReleaseThread, - "eglSwapInterval": &_eglSwapInterval, - "eglSwapBuffers": &_eglSwapBuffers, - "eglTerminate": &_eglTerminate, - "eglQueryString": &_eglQueryString, - "eglWaitClient": &_eglWaitClient, - } - for name, proc := range procs { - p, err := libEGL.FindProc(name) - if err != nil { - return fmt.Errorf("failed to locate %s in %s: %w", name, libEGL.Name, err) - } - *proc = p - } - return nil -} - -func loadDLL(dll *syscall.DLL, name string) error { - handle, err := syscall.LoadLibraryEx(name, 0, syscall.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) - if err != nil { - return fmt.Errorf("egl: failed to load %s: %v", name, err) - } - dll.Handle = handle - dll.Name = name - return nil -} - -func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) { - var cfg _EGLConfig - var ncfg _EGLint - a := &attribs[0] - r, _, _ := _eglChooseConfig.Call(uintptr(disp), uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(&cfg)), 1, uintptr(unsafe.Pointer(&ncfg))) - issue34474KeepAlive(a) - return cfg, r != 0 -} - -func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext { - a := &attribs[0] - c, _, _ := _eglCreateContext.Call(uintptr(disp), uintptr(cfg), uintptr(shareCtx), uintptr(unsafe.Pointer(a))) - issue34474KeepAlive(a) - return _EGLContext(c) -} - -func eglCreateWindowSurface(disp _EGLDisplay, cfg _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface { - a := &attribs[0] - s, _, _ := _eglCreateWindowSurface.Call(uintptr(disp), uintptr(cfg), uintptr(win), uintptr(unsafe.Pointer(a))) - issue34474KeepAlive(a) - return _EGLSurface(s) -} - -func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool { - r, _, _ := _eglDestroySurface.Call(uintptr(disp), uintptr(surf)) - return r != 0 -} - -func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool { - r, _, _ := _eglDestroyContext.Call(uintptr(disp), uintptr(ctx)) - return r != 0 -} - -func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) { - var val uintptr - r, _, _ := _eglGetConfigAttrib.Call(uintptr(disp), uintptr(cfg), uintptr(attr), uintptr(unsafe.Pointer(&val))) - return _EGLint(val), r != 0 -} - -func eglGetDisplay(disp NativeDisplayType) _EGLDisplay { - d, _, _ := _eglGetDisplay.Call(uintptr(disp)) - return _EGLDisplay(d) -} - -func eglGetError() _EGLint { - e, _, _ := _eglGetError.Call() - return _EGLint(e) -} - -func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) { - var maj, min uintptr - r, _, _ := _eglInitialize.Call(uintptr(disp), uintptr(unsafe.Pointer(&maj)), uintptr(unsafe.Pointer(&min))) - return _EGLint(maj), _EGLint(min), r != 0 -} - -func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool { - r, _, _ := _eglMakeCurrent.Call(uintptr(disp), uintptr(draw), uintptr(read), uintptr(ctx)) - return r != 0 -} - -func eglReleaseThread() bool { - r, _, _ := _eglReleaseThread.Call() - return r != 0 -} - -func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool { - r, _, _ := _eglSwapInterval.Call(uintptr(disp), uintptr(interval)) - return r != 0 -} - -func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool { - r, _, _ := _eglSwapBuffers.Call(uintptr(disp), uintptr(surf)) - return r != 0 -} - -func eglTerminate(disp _EGLDisplay) bool { - r, _, _ := _eglTerminate.Call(uintptr(disp)) - return r != 0 -} - -func eglQueryString(disp _EGLDisplay, name _EGLint) string { - r, _, _ := _eglQueryString.Call(uintptr(disp), uintptr(name)) - return syscall.BytePtrToString((*byte)(unsafe.Pointer(r))) -} - -func eglWaitClient() bool { - r, _, _ := _eglWaitClient.Call() - return r != 0 -} - -// issue34474KeepAlive calls runtime.KeepAlive as a -// workaround for golang.org/issue/34474. -func issue34474KeepAlive(v any) { - runtime.KeepAlive(v) -} diff --git a/gio/internal/f32/f32.go b/gio/internal/f32/f32.go deleted file mode 100644 index 06fa60d..0000000 --- a/gio/internal/f32/f32.go +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package f32 is an internal version of the public package f32 with -extra types for internal use. -*/ -package f32 - -import ( - "image" - "math" - - "github.com/p9c/p9/pkg/gel/gio/f32" -) - -type Point = f32.Point - -type Affine2D = f32.Affine2D - -var NewAffine2D = f32.NewAffine2D - -var AffineId = f32.AffineId - -// A Rectangle contains the points (X, Y) where Min.X <= X < Max.X, -// Min.Y <= Y < Max.Y. -type Rectangle struct { - Min, Max Point -} - -// String return a string representation of r. -func (r Rectangle) String() string { - return r.Min.String() + "-" + r.Max.String() -} - -// Rect is a shorthand for Rectangle{Point{x0, y0}, Point{x1, y1}}. -// The returned Rectangle has x0 and y0 swapped if necessary so that -// it's correctly formed. -func Rect(x0, y0, x1, y1 float32) Rectangle { - if x0 > x1 { - x0, x1 = x1, x0 - } - if y0 > y1 { - y0, y1 = y1, y0 - } - return Rectangle{Point{x0, y0}, Point{x1, y1}} -} - -// Pt is shorthand for Point{X: x, Y: y}. -var Pt = f32.Pt - -// Size returns r's width and height. -func (r Rectangle) Size() Point { - return Point{X: r.Dx(), Y: r.Dy()} -} - -// Dx returns r's width. -func (r Rectangle) Dx() float32 { - return r.Max.X - r.Min.X -} - -// Dy returns r's Height. -func (r Rectangle) Dy() float32 { - return r.Max.Y - r.Min.Y -} - -// Intersect returns the intersection of r and s. -func (r Rectangle) Intersect(s Rectangle) Rectangle { - if r.Min.X < s.Min.X { - r.Min.X = s.Min.X - } - if r.Min.Y < s.Min.Y { - r.Min.Y = s.Min.Y - } - if r.Max.X > s.Max.X { - r.Max.X = s.Max.X - } - if r.Max.Y > s.Max.Y { - r.Max.Y = s.Max.Y - } - if r.Empty() { - return Rectangle{} - } - return r -} - -// Union returns the union of r and s. -func (r Rectangle) Union(s Rectangle) Rectangle { - if r.Empty() { - return s - } - if s.Empty() { - return r - } - if r.Min.X > s.Min.X { - r.Min.X = s.Min.X - } - if r.Min.Y > s.Min.Y { - r.Min.Y = s.Min.Y - } - if r.Max.X < s.Max.X { - r.Max.X = s.Max.X - } - if r.Max.Y < s.Max.Y { - r.Max.Y = s.Max.Y - } - return r -} - -// Canon returns the canonical version of r, where Min is to -// the upper left of Max. -func (r Rectangle) Canon() Rectangle { - if r.Max.X < r.Min.X { - r.Min.X, r.Max.X = r.Max.X, r.Min.X - } - if r.Max.Y < r.Min.Y { - r.Min.Y, r.Max.Y = r.Max.Y, r.Min.Y - } - return r -} - -// Empty reports whether r represents the empty area. -func (r Rectangle) Empty() bool { - return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y -} - -// Add offsets r with the vector p. -func (r Rectangle) Add(p Point) Rectangle { - return Rectangle{ - Point{r.Min.X + p.X, r.Min.Y + p.Y}, - Point{r.Max.X + p.X, r.Max.Y + p.Y}, - } -} - -// Sub offsets r with the vector -p. -func (r Rectangle) Sub(p Point) Rectangle { - return Rectangle{ - Point{r.Min.X - p.X, r.Min.Y - p.Y}, - Point{r.Max.X - p.X, r.Max.Y - p.Y}, - } -} - -// Round returns the smallest integer rectangle that -// contains r. -func (r Rectangle) Round() image.Rectangle { - return image.Rectangle{ - Min: image.Point{ - X: int(floor(r.Min.X)), - Y: int(floor(r.Min.Y)), - }, - Max: image.Point{ - X: int(ceil(r.Max.X)), - Y: int(ceil(r.Max.Y)), - }, - } -} - -// fRect converts a rectangle to a f32internal.Rectangle. -func FRect(r image.Rectangle) Rectangle { - return Rectangle{ - Min: FPt(r.Min), Max: FPt(r.Max), - } -} - -// Fpt converts an point to a f32.Point. -func FPt(p image.Point) Point { - return Point{ - X: float32(p.X), Y: float32(p.Y), - } -} - -func ceil(v float32) int { - return int(math.Ceil(float64(v))) -} - -func floor(v float32) int { - return int(math.Floor(float64(v))) -} diff --git a/gio/internal/f32color/f32colorgen/main.go b/gio/internal/f32color/f32colorgen/main.go deleted file mode 100644 index 9407d88..0000000 --- a/gio/internal/f32color/f32colorgen/main.go +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package main - -import ( - "bytes" - "flag" - "fmt" - "go/format" - "math" - "os" -) - -func main() { - out := flag.String("out", "", "output file") - flag.Parse() - - var b bytes.Buffer - printf := func(content string, args ...any) { - fmt.Fprintf(&b, content, args...) - } - - printf("// SPDX-License-Identifier: Unlicense OR MIT\n\n") - printf("// Code generated by f32colorgen. DO NOT EDIT.\n") - printf("\n") - printf("package f32color\n\n") - - printf("// table corresponds to sRGBToLinear(float32(index)/0xff)\n") - printf("var srgb8ToLinear = [...]float32{") - for b := 0; b <= 0xFF; b++ { - if b%0x10 == 0 { - printf("\n\t") - } - v := sRGBToLinear(float32(b) / 0xff) - printf("%#v,", v) - } - printf("\n}\n") - - data, err := format.Source(b.Bytes()) - if err != nil { - fmt.Fprint(os.Stderr, b.String()) - panic(err) - } - - err = os.WriteFile(*out, data, 0o755) - if err != nil { - panic(err) - } -} - -// sRGBToLinear transforms color value from sRGB to linear. -func sRGBToLinear(c float32) float32 { - // Formula from EXT_sRGB. - if c <= 0.04045 { - return c / 12.92 - } else { - return float32(math.Pow(float64((c+0.055)/1.055), 2.4)) - } -} diff --git a/gio/internal/f32color/rgba.go b/gio/internal/f32color/rgba.go deleted file mode 100644 index 5488a0c..0000000 --- a/gio/internal/f32color/rgba.go +++ /dev/null @@ -1,191 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package f32color - -import ( - "image/color" - "math" -) - -//go:generate go run ./f32colorgen -out tables.go - -// RGBA is a 32 bit floating point linear premultiplied color space. -type RGBA struct { - R, G, B, A float32 -} - -// Array returns rgba values in a [4]float32 array. -func (rgba RGBA) Array() [4]float32 { - return [4]float32{rgba.R, rgba.G, rgba.B, rgba.A} -} - -// Float32 returns r, g, b, a values. -func (col RGBA) Float32() (r, g, b, a float32) { - return col.R, col.G, col.B, col.A -} - -// SRGBA converts from linear to sRGB color space. -func (col RGBA) SRGB() color.NRGBA { - if col.A == 0 { - return color.NRGBA{} - } - return color.NRGBA{ - R: uint8(linearTosRGB(col.R/col.A)*255 + .5), - G: uint8(linearTosRGB(col.G/col.A)*255 + .5), - B: uint8(linearTosRGB(col.B/col.A)*255 + .5), - A: uint8(col.A*255 + .5), - } -} - -// Luminance calculates the relative luminance of a linear RGBA color. -// Normalized to 0 for black and 1 for white. -// -// See https://www.w3.org/TR/WCAG20/#relativeluminancedef for more details -func (col RGBA) Luminance() float32 { - return 0.2126*col.R + 0.7152*col.G + 0.0722*col.B -} - -// Opaque returns the color without alpha component. -func (col RGBA) Opaque() RGBA { - col.A = 1.0 - return col -} - -// LinearFromSRGB converts from col in the sRGB colorspace to RGBA. -func LinearFromSRGB(col color.NRGBA) RGBA { - af := float32(col.A) / 0xFF - return RGBA{ - R: srgb8ToLinear[col.R] * af, // sRGBToLinear(float32(col.R)/0xff) * af, - G: srgb8ToLinear[col.G] * af, // sRGBToLinear(float32(col.G)/0xff) * af, - B: srgb8ToLinear[col.B] * af, // sRGBToLinear(float32(col.B)/0xff) * af, - A: af, - } -} - -// NRGBAToRGBA converts from non-premultiplied sRGB color to premultiplied sRGB color. -// -// Each component in the result is `sRGBToLinear(c * alpha)`, where `c` -// is the linear color. -func NRGBAToRGBA(col color.NRGBA) color.RGBA { - if col.A == 0xFF { - return color.RGBA(col) - } - c := LinearFromSRGB(col) - return color.RGBA{ - R: uint8(linearTosRGB(c.R)*255 + .5), - G: uint8(linearTosRGB(c.G)*255 + .5), - B: uint8(linearTosRGB(c.B)*255 + .5), - A: col.A, - } -} - -// NRGBAToLinearRGBA converts from non-premultiplied sRGB color to premultiplied linear RGBA color. -// -// Each component in the result is `c * alpha`, where `c` is the linear color. -func NRGBAToLinearRGBA(col color.NRGBA) color.RGBA { - if col.A == 0xFF { - return color.RGBA(col) - } - c := LinearFromSRGB(col) - return color.RGBA{ - R: uint8(c.R*255 + .5), - G: uint8(c.G*255 + .5), - B: uint8(c.B*255 + .5), - A: col.A, - } -} - -// RGBAToNRGBA converts from premultiplied sRGB color to non-premultiplied sRGB color. -func RGBAToNRGBA(col color.RGBA) color.NRGBA { - if col.A == 0xFF { - return color.NRGBA(col) - } - - linear := RGBA{ - R: sRGBToLinear(float32(col.R) / 0xff), - G: sRGBToLinear(float32(col.G) / 0xff), - B: sRGBToLinear(float32(col.B) / 0xff), - A: float32(col.A) / 0xff, - } - - return linear.SRGB() -} - -// linearTosRGB transforms color value from linear to sRGB. -func linearTosRGB(c float32) float32 { - // Formula from EXT_sRGB. - switch { - case c <= 0: - return 0 - case 0 < c && c < 0.0031308: - return 12.92 * c - case 0.0031308 <= c && c < 1: - return 1.055*float32(math.Pow(float64(c), 0.41666)) - 0.055 - } - - return 1 -} - -// sRGBToLinear transforms color value from sRGB to linear. -func sRGBToLinear(c float32) float32 { - // Formula from EXT_sRGB. - if c <= 0.04045 { - return c / 12.92 - } else { - return float32(math.Pow(float64((c+0.055)/1.055), 2.4)) - } -} - -// MulAlpha applies the alpha to the color. -func MulAlpha(c color.NRGBA, alpha uint8) color.NRGBA { - c.A = uint8(uint32(c.A) * uint32(alpha) / 0xFF) - return c -} - -// Disabled blends color towards the luminance and multiplies alpha. -// Blending towards luminance will desaturate the color. -// Multiplying alpha blends the color together more with the background. -func Disabled(c color.NRGBA) (d color.NRGBA) { - const r = 80 // blend ratio - lum := approxLuminance(c) - d = mix(c, color.NRGBA{A: c.A, R: lum, G: lum, B: lum}, r) - d = MulAlpha(d, 128+32) - return -} - -// Hovered blends dark colors towards white, and light colors towards -// black. It is approximate because it operates in non-linear sRGB space. -func Hovered(c color.NRGBA) (h color.NRGBA) { - if c.A == 0 { - // Provide a reasonable default for transparent widgets. - return color.NRGBA{A: 0x44, R: 0x88, G: 0x88, B: 0x88} - } - const ratio = 0x20 - m := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: c.A} - if approxLuminance(c) > 128 { - m = color.NRGBA{A: c.A} - } - return mix(m, c, ratio) -} - -// mix mixes c1 and c2 weighted by (1 - a/256) and a/256 respectively. -func mix(c1, c2 color.NRGBA, a uint8) color.NRGBA { - ai := int(a) - return color.NRGBA{ - R: byte((int(c1.R)*ai + int(c2.R)*(256-ai)) / 256), - G: byte((int(c1.G)*ai + int(c2.G)*(256-ai)) / 256), - B: byte((int(c1.B)*ai + int(c2.B)*(256-ai)) / 256), - A: byte((int(c1.A)*ai + int(c2.A)*(256-ai)) / 256), - } -} - -// approxLuminance is a fast approximate version of RGBA.Luminance. -func approxLuminance(c color.NRGBA) byte { - const ( - r = 13933 // 0.2126 * 256 * 256 - g = 46871 // 0.7152 * 256 * 256 - b = 4732 // 0.0722 * 256 * 256 - t = r + g + b - ) - return byte((r*int(c.R) + g*int(c.G) + b*int(c.B)) / t) -} diff --git a/gio/internal/f32color/rgba_test.go b/gio/internal/f32color/rgba_test.go deleted file mode 100644 index 3658a09..0000000 --- a/gio/internal/f32color/rgba_test.go +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package f32color - -import ( - "image/color" - "testing" -) - -func TestNRGBAToLinearRGBA_Boundary(t *testing.T) { - for col := 0; col <= 0xFF; col++ { - for alpha := 0; alpha <= 0xFF; alpha++ { - in := color.NRGBA{R: uint8(col), A: uint8(alpha)} - premul := NRGBAToLinearRGBA(in) - if premul.A != uint8(alpha) { - t.Errorf("%v: got %v expected %v", in, premul.A, alpha) - } - if premul.R > premul.A { - t.Errorf("%v: R=%v > A=%v", in, premul.R, premul.A) - } - } - } -} - -func TestLinearToRGBARoundtrip(t *testing.T) { - for col := 0; col <= 0xFF; col++ { - for alpha := 0; alpha <= 0xFF; alpha++ { - want := color.NRGBA{R: uint8(col), A: uint8(alpha)} - if alpha == 0 { - want.R = 0 - } - got := LinearFromSRGB(want).SRGB() - if want != got { - t.Errorf("got %v expected %v", got, want) - } - } - } -} - -var sink RGBA - -func BenchmarkLinearFromSRGB(b *testing.B) { - b.Run("opaque", func(b *testing.B) { - for i := 0; b.Loop(); i++ { - sink = LinearFromSRGB(color.NRGBA{R: byte(i), G: byte(i >> 8), B: byte(i >> 16), A: 0xFF}) - } - }) - b.Run("translucent", func(b *testing.B) { - for i := 0; b.Loop(); i++ { - sink = LinearFromSRGB(color.NRGBA{R: byte(i), G: byte(i >> 8), B: byte(i >> 16), A: 0x50}) - } - }) - b.Run("transparent", func(b *testing.B) { - for i := 0; b.Loop(); i++ { - sink = LinearFromSRGB(color.NRGBA{R: byte(i), G: byte(i >> 8), B: byte(i >> 16), A: 0x00}) - } - }) -} diff --git a/gio/internal/f32color/tables.go b/gio/internal/f32color/tables.go deleted file mode 100644 index b804786..0000000 --- a/gio/internal/f32color/tables.go +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Code generated by f32colorgen. DO NOT EDIT. - -package f32color - -// table corresponds to sRGBToLinear(float32(index)/0xff) -var srgb8ToLinear = [...]float32{ - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, - 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, -} diff --git a/gio/internal/fling/animation.go b/gio/internal/fling/animation.go deleted file mode 100644 index c76b66f..0000000 --- a/gio/internal/fling/animation.go +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package fling - -import ( - "math" - "runtime" - "time" - - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -type Animation struct { - // Current offset in pixels. - x float32 - // Initial time. - t0 time.Time - // Initial velocity in pixels pr second. - v0 float32 -} - -const ( - // dp/second. - minFlingVelocity = unit.Dp(50) - maxFlingVelocity = unit.Dp(8000) - thresholdVelocity = 1 -) - -// Start a fling given a starting velocity. Returns whether a -// fling was started. -func (f *Animation) Start(c unit.Metric, now time.Time, velocity float32) bool { - min := float32(c.Dp(minFlingVelocity)) - v := velocity - if -min <= v && v <= min { - return false - } - max := float32(c.Dp(maxFlingVelocity)) - if v > max { - v = max - } else if v < -max { - v = -max - } - f.init(now, v) - return true -} - -func (f *Animation) init(now time.Time, v0 float32) { - f.t0 = now - f.v0 = v0 - f.x = 0 -} - -func (f *Animation) Active() bool { - return f.v0 != 0 -} - -// Tick computes and returns a fling distance since -// the last time Tick was called. -func (f *Animation) Tick(now time.Time) int { - if !f.Active() { - return 0 - } - var k float32 - if runtime.GOOS == "darwin" { - k = -2 // iOS - } else { - k = -4.2 // Android and default - } - t := now.Sub(f.t0) - // The acceleration x''(t) of a point mass with a drag - // force, f, proportional with velocity, x'(t), is - // governed by the equation - // - // x''(t) = kx'(t) - // - // Given the starting position x(0) = 0, the starting - // velocity x'(0) = v0, the position is then - // given by - // - // x(t) = v0*e^(k*t)/k - v0/k - // - ekt := float32(math.Exp(float64(k) * t.Seconds())) - x := f.v0*ekt/k - f.v0/k - dist := x - f.x - idist := int(dist) - f.x += float32(idist) - // Solving for the velocity x'(t) gives us - // - // x'(t) = v0*e^(k*t) - v := f.v0 * ekt - if -thresholdVelocity < v && v < thresholdVelocity { - f.v0 = 0 - } - return idist -} diff --git a/gio/internal/fling/extrapolation.go b/gio/internal/fling/extrapolation.go deleted file mode 100644 index d8e2996..0000000 --- a/gio/internal/fling/extrapolation.go +++ /dev/null @@ -1,332 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package fling - -import ( - "math" - "strconv" - "strings" - "time" -) - -// Extrapolation computes a 1-dimensional velocity estimate -// for a set of timestamped points using the least squares -// fit of a 2nd order polynomial. The same method is used -// by Android. -type Extrapolation struct { - // Index into points. - idx int - // Circular buffer of samples. - samples []sample - lastValue float32 - // Pre-allocated cache for samples. - cache [historySize]sample - - // Filtered values and times - values [historySize]float32 - times [historySize]float32 -} - -type sample struct { - t time.Duration - v float32 -} - -type matrix struct { - rows, cols int - data []float32 -} - -type Estimate struct { - Velocity float32 - Distance float32 -} - -type coefficients [degree + 1]float32 - -const ( - degree = 2 - historySize = 20 - maxAge = 100 * time.Millisecond - maxSampleGap = 40 * time.Millisecond -) - -// SampleDelta adds a relative sample to the estimation. -func (e *Extrapolation) SampleDelta(t time.Duration, delta float32) { - val := delta + e.lastValue - e.Sample(t, val) -} - -// Sample adds an absolute sample to the estimation. -func (e *Extrapolation) Sample(t time.Duration, val float32) { - e.lastValue = val - if e.samples == nil { - e.samples = e.cache[:0] - } - s := sample{ - t: t, - v: val, - } - if e.idx == len(e.samples) && e.idx < cap(e.samples) { - e.samples = append(e.samples, s) - } else { - e.samples[e.idx] = s - } - e.idx++ - if e.idx == cap(e.samples) { - e.idx = 0 - } -} - -// Velocity returns an estimate of the implied velocity and -// distance for the points sampled, or zero if the estimation method -// failed. -func (e *Extrapolation) Estimate() Estimate { - if len(e.samples) == 0 { - return Estimate{} - } - values := e.values[:0] - times := e.times[:0] - first := e.get(0) - t := first.t - // Walk backwards collecting samples. - for i := range e.samples { - p := e.get(-i) - age := first.t - p.t - if age >= maxAge || t-p.t >= maxSampleGap { - // If the samples are too old or - // too much time passed between samples - // assume they're not part of the fling. - break - } - t = p.t - values = append(values, first.v-p.v) - times = append(times, float32((-age).Seconds())) - } - coef, ok := polyFit(times, values) - if !ok { - return Estimate{} - } - dist := values[len(values)-1] - values[0] - return Estimate{ - Velocity: coef[1], - Distance: dist, - } -} - -func (e *Extrapolation) get(i int) sample { - idx := (e.idx + i - 1 + len(e.samples)) % len(e.samples) - return e.samples[idx] -} - -// fit computes the least squares polynomial fit for -// the set of points in X, Y. If the fitting fails -// because of contradicting or insufficient data, -// fit returns false. -func polyFit(X, Y []float32) (coefficients, bool) { - if len(X) != len(Y) { - panic("X and Y lengths differ") - } - if len(X) <= degree { - // Not enough points to fit a curve. - return coefficients{}, false - } - - // Use a method similar to Android's VelocityTracker.cpp: - // https://android.googlesource.com/platform/frameworks/base/+/56a2301/libs/androidfw/VelocityTracker.cpp - // where all weights are 1. - - // First, expand the X vector to the matrix A in column-major order. - A := newMatrix(degree+1, len(X)) - for i, x := range X { - A.set(0, i, 1) - for j := 1; j < A.rows; j++ { - A.set(j, i, A.get(j-1, i)*x) - } - } - - Q, Rt, ok := decomposeQR(A) - if !ok { - return coefficients{}, false - } - // Solve R*B = Qt*Y for B, which is then the polynomial coefficients. - // Since R is upper triangular, we can proceed from bottom right to - // upper left. - // https://en.wikipedia.org/wiki/Non-linear_least_squares - var B coefficients - for i := Q.rows - 1; i >= 0; i-- { - B[i] = dot(Q.col(i), Y) - for j := Q.rows - 1; j > i; j-- { - B[i] -= Rt.get(i, j) * B[j] - } - B[i] /= Rt.get(i, i) - } - return B, true -} - -// decomposeQR computes and returns Q, Rt where Q*transpose(Rt) = A, if -// possible. R is guaranteed to be upper triangular and only the square -// part of Rt is returned. -func decomposeQR(A *matrix) (*matrix, *matrix, bool) { - // Gram-Schmidt QR decompose A where Q*R = A. - // https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process - Q := newMatrix(A.rows, A.cols) // Column-major. - Rt := newMatrix(A.rows, A.rows) // R transposed, row-major. - for i := range Q.rows { - // Copy A column. - for j := range Q.cols { - Q.set(i, j, A.get(i, j)) - } - // Subtract projections. Note that int the projection - // - // proju a = / u - // - // the normalized column e replaces u, where = 1: - // - // proje a = / e = e - for j := range i { - d := dot(Q.col(j), Q.col(i)) - for k := range Q.cols { - Q.set(i, k, Q.get(i, k)-d*Q.get(j, k)) - } - } - // Normalize Q columns. - n := norm(Q.col(i)) - if n < 0.000001 { - // Degenerate data, no solution. - return nil, nil, false - } - invNorm := 1 / n - for j := range Q.cols { - Q.set(i, j, Q.get(i, j)*invNorm) - } - // Update Rt. - for j := i; j < Rt.cols; j++ { - Rt.set(i, j, dot(Q.col(i), A.col(j))) - } - } - return Q, Rt, true -} - -func norm(V []float32) float32 { - var n float32 - for _, v := range V { - n += v * v - } - return float32(math.Sqrt(float64(n))) -} - -func dot(V1, V2 []float32) float32 { - var d float32 - for i, v1 := range V1 { - d += v1 * V2[i] - } - return d -} - -func newMatrix(rows, cols int) *matrix { - return &matrix{ - rows: rows, - cols: cols, - data: make([]float32, rows*cols), - } -} - -func (m *matrix) set(row, col int, v float32) { - if row < 0 || row >= m.rows { - panic("row out of range") - } - if col < 0 || col >= m.cols { - panic("col out of range") - } - m.data[row*m.cols+col] = v -} - -func (m *matrix) get(row, col int) float32 { - if row < 0 || row >= m.rows { - panic("row out of range") - } - if col < 0 || col >= m.cols { - panic("col out of range") - } - return m.data[row*m.cols+col] -} - -func (m *matrix) col(c int) []float32 { - return m.data[c*m.cols : (c+1)*m.cols] -} - -func (m *matrix) approxEqual(m2 *matrix) bool { - if m.rows != m2.rows || m.cols != m2.cols { - return false - } - const epsilon = 0.00001 - for row := range m.rows { - for col := range m.cols { - d := m2.get(row, col) - m.get(row, col) - if d < -epsilon || d > epsilon { - return false - } - } - } - return true -} - -func (m *matrix) transpose() *matrix { - t := &matrix{ - rows: m.cols, - cols: m.rows, - data: make([]float32, len(m.data)), - } - for i := range m.rows { - for j := range m.cols { - t.set(j, i, m.get(i, j)) - } - } - return t -} - -func (m *matrix) mul(m2 *matrix) *matrix { - if m.rows != m2.cols { - panic("mismatched matrices") - } - mm := &matrix{ - rows: m.rows, - cols: m2.cols, - data: make([]float32, m.rows*m2.cols), - } - for i := range mm.rows { - for j := range mm.cols { - var v float32 - for k := range m.rows { - v += m.get(k, j) * m2.get(i, k) - } - mm.set(i, j, v) - } - } - return mm -} - -func (m *matrix) String() string { - var b strings.Builder - for i := range m.rows { - for j := range m.cols { - v := m.get(i, j) - b.WriteString(strconv.FormatFloat(float64(v), 'g', -1, 32)) - b.WriteString(", ") - } - b.WriteString("\n") - } - return b.String() -} - -func (c coefficients) approxEqual(c2 coefficients) bool { - const epsilon = 0.00001 - for i, v := range c { - d := v - c2[i] - if d < -epsilon || d > epsilon { - return false - } - } - return true -} diff --git a/gio/internal/fling/extrapolation_test.go b/gio/internal/fling/extrapolation_test.go deleted file mode 100644 index 3f9d982..0000000 --- a/gio/internal/fling/extrapolation_test.go +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package fling - -import "testing" - -func TestDecomposeQR(t *testing.T) { - A := &matrix{ - rows: 3, cols: 3, - data: []float32{ - 12, 6, -4, - -51, 167, 24, - 4, -68, -41, - }, - } - Q, Rt, ok := decomposeQR(A) - if !ok { - t.Fatal("decomposeQR failed") - } - R := Rt.transpose() - QR := Q.mul(R) - if !A.approxEqual(QR) { - t.Log("A\n", A) - t.Log("Q\n", Q) - t.Log("R\n", R) - t.Log("QR\n", QR) - t.Fatal("Q*R not approximately equal to A") - } -} - -func TestFit(t *testing.T) { - X := []float32{-1, 0, 1} - Y := []float32{2, 0, 2} - - got, ok := polyFit(X, Y) - if !ok { - t.Fatal("polyFit failed") - } - want := coefficients{0, 0, 2} - if !got.approxEqual(want) { - t.Fatalf("polyFit: got %v want %v", got, want) - } -} diff --git a/gio/internal/gl/gl.go b/gio/internal/gl/gl.go deleted file mode 100644 index c1b487e..0000000 --- a/gio/internal/gl/gl.go +++ /dev/null @@ -1,131 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gl - -type ( - Attrib uint - Enum uint -) - -const ( - ACTIVE_TEXTURE = 0x84E0 - ALL_BARRIER_BITS = 0xffffffff - ARRAY_BUFFER = 0x8892 - ARRAY_BUFFER_BINDING = 0x8894 - BACK = 0x0405 - BLEND = 0xbe2 - BLEND_DST_RGB = 0x80C8 - BLEND_SRC_RGB = 0x80C9 - BLEND_DST_ALPHA = 0x80CA - BLEND_SRC_ALPHA = 0x80CB - CLAMP_TO_EDGE = 0x812f - COLOR_ATTACHMENT0 = 0x8ce0 - COLOR_BUFFER_BIT = 0x4000 - COLOR_CLEAR_VALUE = 0x0C22 - COMPILE_STATUS = 0x8b81 - COMPUTE_SHADER = 0x91B9 - CURRENT_PROGRAM = 0x8B8D - DEPTH_ATTACHMENT = 0x8d00 - DEPTH_BUFFER_BIT = 0x100 - DEPTH_CLEAR_VALUE = 0x0B73 - DEPTH_COMPONENT16 = 0x81a5 - DEPTH_COMPONENT24 = 0x81A6 - DEPTH_COMPONENT32F = 0x8CAC - DEPTH_FUNC = 0x0B74 - DEPTH_TEST = 0xb71 - DEPTH_WRITEMASK = 0x0B72 - DRAW_FRAMEBUFFER = 0x8CA9 - DST_COLOR = 0x306 - DYNAMIC_DRAW = 0x88E8 - DYNAMIC_READ = 0x88E9 - ELEMENT_ARRAY_BUFFER = 0x8893 - ELEMENT_ARRAY_BUFFER_BINDING = 0x8895 - EXTENSIONS = 0x1f03 - FALSE = 0 - FLOAT = 0x1406 - FRAGMENT_SHADER = 0x8b30 - FRAMEBUFFER = 0x8d40 - FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING = 0x8210 - FRAMEBUFFER_BINDING = 0x8ca6 - FRAMEBUFFER_COMPLETE = 0x8cd5 - FRAMEBUFFER_SRGB = 0x8db9 - HALF_FLOAT = 0x140b - HALF_FLOAT_OES = 0x8d61 - INFO_LOG_LENGTH = 0x8B84 - INVALID_INDEX = ^uint(0) - GREATER = 0x204 - GEQUAL = 0x206 - LINEAR = 0x2601 - LINEAR_MIPMAP_LINEAR = 0x2703 - LINK_STATUS = 0x8b82 - LUMINANCE = 0x1909 - MAP_READ_BIT = 0x0001 - MAX_TEXTURE_SIZE = 0xd33 - NEAREST = 0x2600 - NO_ERROR = 0x0 - NUM_EXTENSIONS = 0x821D - ONE = 0x1 - ONE_MINUS_SRC_ALPHA = 0x303 - PACK_ROW_LENGTH = 0x0D02 - PROGRAM_BINARY_LENGTH = 0x8741 - QUERY_RESULT = 0x8866 - QUERY_RESULT_AVAILABLE = 0x8867 - R16F = 0x822d - R8 = 0x8229 - READ_FRAMEBUFFER = 0x8ca8 - READ_FRAMEBUFFER_BINDING = 0x8CAA - READ_ONLY = 0x88B8 - READ_WRITE = 0x88BA - RED = 0x1903 - RENDERER = 0x1F01 - RENDERBUFFER = 0x8d41 - RENDERBUFFER_BINDING = 0x8ca7 - RENDERBUFFER_HEIGHT = 0x8d43 - RENDERBUFFER_WIDTH = 0x8d42 - RGB = 0x1907 - RGBA = 0x1908 - RGBA8 = 0x8058 - SHADER_STORAGE_BUFFER = 0x90D2 - SHADER_STORAGE_BUFFER_BINDING = 0x90D3 - SHORT = 0x1402 - SRGB = 0x8c40 - SRGB_ALPHA_EXT = 0x8c42 - SRGB8 = 0x8c41 - SRGB8_ALPHA8 = 0x8c43 - STATIC_DRAW = 0x88e4 - STENCIL_BUFFER_BIT = 0x00000400 - TEXTURE_2D = 0xde1 - TEXTURE_BINDING_2D = 0x8069 - TEXTURE_MAG_FILTER = 0x2800 - TEXTURE_MIN_FILTER = 0x2801 - TEXTURE_WRAP_S = 0x2802 - TEXTURE_WRAP_T = 0x2803 - TEXTURE0 = 0x84c0 - TEXTURE1 = 0x84c1 - TRIANGLE_STRIP = 0x5 - TRIANGLES = 0x4 - TRUE = 1 - UNIFORM_BUFFER = 0x8A11 - UNIFORM_BUFFER_BINDING = 0x8A28 - UNPACK_ALIGNMENT = 0xcf5 - UNPACK_ROW_LENGTH = 0x0CF2 - UNSIGNED_BYTE = 0x1401 - UNSIGNED_SHORT = 0x1403 - VIEWPORT = 0x0BA2 - VERSION = 0x1f02 - VERTEX_ARRAY_BINDING = 0x85B5 - VERTEX_SHADER = 0x8b31 - VERTEX_ATTRIB_ARRAY_BUFFER_BINDING = 0x889F - VERTEX_ATTRIB_ARRAY_ENABLED = 0x8622 - VERTEX_ATTRIB_ARRAY_POINTER = 0x8645 - VERTEX_ATTRIB_ARRAY_NORMALIZED = 0x886A - VERTEX_ATTRIB_ARRAY_SIZE = 0x8623 - VERTEX_ATTRIB_ARRAY_STRIDE = 0x8624 - VERTEX_ATTRIB_ARRAY_TYPE = 0x8625 - WRITE_ONLY = 0x88B9 - ZERO = 0x0 - - // EXT_disjoint_timer_query - TIME_ELAPSED_EXT = 0x88BF - GPU_DISJOINT_EXT = 0x8FBB -) diff --git a/gio/internal/gl/gl_js.go b/gio/internal/gl/gl_js.go deleted file mode 100644 index f2d794a..0000000 --- a/gio/internal/gl/gl_js.go +++ /dev/null @@ -1,748 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gl - -import ( - "errors" - "strings" - "syscall/js" -) - -type Functions struct { - Ctx js.Value - EXT_disjoint_timer_query js.Value - EXT_disjoint_timer_query_webgl2 js.Value - - // Cached reference to the Uint8Array JS type. - uint8Array js.Value - - // Cached JS arrays. - arrayBuf js.Value - int32Buf js.Value - - isWebGL2 bool - - _getExtension js.Value - _activeTexture js.Value - _attachShader js.Value - _beginQuery js.Value - _beginQueryEXT js.Value - _bindAttribLocation js.Value - _bindBuffer js.Value - _bindBufferBase js.Value - _bindFramebuffer js.Value - _bindRenderbuffer js.Value - _bindTexture js.Value - _blendEquation js.Value - _blendFunc js.Value - _bufferData js.Value - _bufferSubData js.Value - _checkFramebufferStatus js.Value - _clear js.Value - _clearColor js.Value - _clearDepth js.Value - _compileShader js.Value - _copyTexSubImage2D js.Value - _createBuffer js.Value - _createFramebuffer js.Value - _createProgram js.Value - _createQuery js.Value - _createRenderbuffer js.Value - _createShader js.Value - _createTexture js.Value - _deleteBuffer js.Value - _deleteFramebuffer js.Value - _deleteProgram js.Value - _deleteQuery js.Value - _deleteQueryEXT js.Value - _deleteShader js.Value - _deleteRenderbuffer js.Value - _deleteTexture js.Value - _depthFunc js.Value - _depthMask js.Value - _disableVertexAttribArray js.Value - _disable js.Value - _drawArrays js.Value - _drawElements js.Value - _enable js.Value - _enableVertexAttribArray js.Value - _endQuery js.Value - _endQueryEXT js.Value - _finish js.Value - _flush js.Value - _framebufferRenderbuffer js.Value - _framebufferTexture2D js.Value - _generateMipmap js.Value - _getRenderbufferParameteri js.Value - _getFramebufferAttachmentParameter js.Value - _getParameter js.Value - _getIndexedParameter js.Value - _getProgramParameter js.Value - _getProgramInfoLog js.Value - _getQueryParameter js.Value - _getQueryObjectEXT js.Value - _getShaderParameter js.Value - _getShaderInfoLog js.Value - _getSupportedExtensions js.Value - _getUniformBlockIndex js.Value - _getUniformLocation js.Value - _getVertexAttrib js.Value - _getVertexAttribOffset js.Value - _invalidateFramebuffer js.Value - _isEnabled js.Value - _linkProgram js.Value - _pixelStorei js.Value - _renderbufferStorage js.Value - _readPixels js.Value - _scissor js.Value - _shaderSource js.Value - _texImage2D js.Value - _texStorage2D js.Value - _texSubImage2D js.Value - _texParameteri js.Value - _uniformBlockBinding js.Value - _uniform1f js.Value - _uniform1i js.Value - _uniform2f js.Value - _uniform3f js.Value - _uniform4f js.Value - _useProgram js.Value - _vertexAttribPointer js.Value - _viewport js.Value -} - -type Context js.Value - -func NewFunctions(ctx Context, forceES bool) (*Functions, error) { - webgl := js.Value(ctx) - f := &Functions{ - Ctx: webgl, - uint8Array: js.Global().Get("Uint8Array"), - _getExtension: _bind(webgl, `getExtension`), - _activeTexture: _bind(webgl, `activeTexture`), - _attachShader: _bind(webgl, `attachShader`), - _beginQuery: _bind(webgl, `beginQuery`), - _beginQueryEXT: _bind(webgl, `beginQueryEXT`), - _bindAttribLocation: _bind(webgl, `bindAttribLocation`), - _bindBuffer: _bind(webgl, `bindBuffer`), - _bindBufferBase: _bind(webgl, `bindBufferBase`), - _bindFramebuffer: _bind(webgl, `bindFramebuffer`), - _bindRenderbuffer: _bind(webgl, `bindRenderbuffer`), - _bindTexture: _bind(webgl, `bindTexture`), - _blendEquation: _bind(webgl, `blendEquation`), - _blendFunc: _bind(webgl, `blendFunc`), - _bufferData: _bind(webgl, `bufferData`), - _bufferSubData: _bind(webgl, `bufferSubData`), - _checkFramebufferStatus: _bind(webgl, `checkFramebufferStatus`), - _clear: _bind(webgl, `clear`), - _clearColor: _bind(webgl, `clearColor`), - _clearDepth: _bind(webgl, `clearDepth`), - _compileShader: _bind(webgl, `compileShader`), - _copyTexSubImage2D: _bind(webgl, `copyTexSubImage2D`), - _createBuffer: _bind(webgl, `createBuffer`), - _createFramebuffer: _bind(webgl, `createFramebuffer`), - _createProgram: _bind(webgl, `createProgram`), - _createQuery: _bind(webgl, `createQuery`), - _createRenderbuffer: _bind(webgl, `createRenderbuffer`), - _createShader: _bind(webgl, `createShader`), - _createTexture: _bind(webgl, `createTexture`), - _deleteBuffer: _bind(webgl, `deleteBuffer`), - _deleteFramebuffer: _bind(webgl, `deleteFramebuffer`), - _deleteProgram: _bind(webgl, `deleteProgram`), - _deleteQuery: _bind(webgl, `deleteQuery`), - _deleteQueryEXT: _bind(webgl, `deleteQueryEXT`), - _deleteShader: _bind(webgl, `deleteShader`), - _deleteRenderbuffer: _bind(webgl, `deleteRenderbuffer`), - _deleteTexture: _bind(webgl, `deleteTexture`), - _depthFunc: _bind(webgl, `depthFunc`), - _depthMask: _bind(webgl, `depthMask`), - _disableVertexAttribArray: _bind(webgl, `disableVertexAttribArray`), - _disable: _bind(webgl, `disable`), - _drawArrays: _bind(webgl, `drawArrays`), - _drawElements: _bind(webgl, `drawElements`), - _enable: _bind(webgl, `enable`), - _enableVertexAttribArray: _bind(webgl, `enableVertexAttribArray`), - _endQuery: _bind(webgl, `endQuery`), - _endQueryEXT: _bind(webgl, `endQueryEXT`), - _finish: _bind(webgl, `finish`), - _flush: _bind(webgl, `flush`), - _framebufferRenderbuffer: _bind(webgl, `framebufferRenderbuffer`), - _framebufferTexture2D: _bind(webgl, `framebufferTexture2D`), - _generateMipmap: _bind(webgl, `generateMipmap`), - _getRenderbufferParameteri: _bind(webgl, `getRenderbufferParameteri`), - _getFramebufferAttachmentParameter: _bind(webgl, `getFramebufferAttachmentParameter`), - _getParameter: _bind(webgl, `getParameter`), - _getIndexedParameter: _bind(webgl, `getIndexedParameter`), - _getProgramParameter: _bind(webgl, `getProgramParameter`), - _getProgramInfoLog: _bind(webgl, `getProgramInfoLog`), - _getQueryParameter: _bind(webgl, `getQueryParameter`), - _getQueryObjectEXT: _bind(webgl, `getQueryObjectEXT`), - _getShaderParameter: _bind(webgl, `getShaderParameter`), - _getShaderInfoLog: _bind(webgl, `getShaderInfoLog`), - _getSupportedExtensions: _bind(webgl, `getSupportedExtensions`), - _getUniformBlockIndex: _bind(webgl, `getUniformBlockIndex`), - _getUniformLocation: _bind(webgl, `getUniformLocation`), - _getVertexAttrib: _bind(webgl, `getVertexAttrib`), - _getVertexAttribOffset: _bind(webgl, `getVertexAttribOffset`), - _invalidateFramebuffer: _bind(webgl, `invalidateFramebuffer`), - _isEnabled: _bind(webgl, `isEnabled`), - _linkProgram: _bind(webgl, `linkProgram`), - _pixelStorei: _bind(webgl, `pixelStorei`), - _renderbufferStorage: _bind(webgl, `renderbufferStorage`), - _readPixels: _bind(webgl, `readPixels`), - _scissor: _bind(webgl, `scissor`), - _shaderSource: _bind(webgl, `shaderSource`), - _texImage2D: _bind(webgl, `texImage2D`), - _texStorage2D: _bind(webgl, `texStorage2D`), - _texSubImage2D: _bind(webgl, `texSubImage2D`), - _texParameteri: _bind(webgl, `texParameteri`), - _uniformBlockBinding: _bind(webgl, `uniformBlockBinding`), - _uniform1f: _bind(webgl, `uniform1f`), - _uniform1i: _bind(webgl, `uniform1i`), - _uniform2f: _bind(webgl, `uniform2f`), - _uniform3f: _bind(webgl, `uniform3f`), - _uniform4f: _bind(webgl, `uniform4f`), - _useProgram: _bind(webgl, `useProgram`), - _vertexAttribPointer: _bind(webgl, `vertexAttribPointer`), - _viewport: _bind(webgl, `viewport`), - } - if err := f.Init(); err != nil { - return nil, err - } - return f, nil -} - -func _bind(ctx js.Value, p string) js.Value { - if o := ctx.Get(p); o.Truthy() { - return o.Call("bind", ctx) - } - return js.Undefined() -} - -func (f *Functions) Init() error { - webgl2Class := js.Global().Get("WebGL2RenderingContext") - f.isWebGL2 = !webgl2Class.IsUndefined() && f.Ctx.InstanceOf(webgl2Class) - if !f.isWebGL2 { - f.EXT_disjoint_timer_query = f.getExtension("EXT_disjoint_timer_query") - if f.getExtension("OES_texture_half_float").IsNull() && f.getExtension("OES_texture_float").IsNull() { - return errors.New("gl: no support for neither OES_texture_half_float nor OES_texture_float") - } - if f.getExtension("EXT_sRGB").IsNull() { - return errors.New("gl: EXT_sRGB not supported") - } - } else { - // WebGL2 extensions. - f.EXT_disjoint_timer_query_webgl2 = f.getExtension("EXT_disjoint_timer_query_webgl2") - if f.getExtension("EXT_color_buffer_half_float").IsNull() && f.getExtension("EXT_color_buffer_float").IsNull() { - return errors.New("gl: no support for neither EXT_color_buffer_half_float nor EXT_color_buffer_float") - } - } - return nil -} - -func (f *Functions) getExtension(name string) js.Value { - return f._getExtension.Invoke(name) -} - -func (f *Functions) ActiveTexture(t Enum) { - f._activeTexture.Invoke(int(t)) -} - -func (f *Functions) AttachShader(p Program, s Shader) { - f._attachShader.Invoke(js.Value(p), js.Value(s)) -} - -func (f *Functions) BeginQuery(target Enum, query Query) { - if !f.EXT_disjoint_timer_query_webgl2.IsNull() { - f._beginQuery.Invoke(int(target), js.Value(query)) - } else { - f.EXT_disjoint_timer_query.Call("beginQueryEXT", int(target), js.Value(query)) - } -} - -func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { - f._bindAttribLocation.Invoke(js.Value(p), int(a), name) -} - -func (f *Functions) BindBuffer(target Enum, b Buffer) { - f._bindBuffer.Invoke(int(target), js.Value(b)) -} - -func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) { - f._bindBufferBase.Invoke(int(target), index, js.Value(b)) -} - -func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { - f._bindFramebuffer.Invoke(int(target), js.Value(fb)) -} - -func (f *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { - f._bindRenderbuffer.Invoke(int(target), js.Value(rb)) -} - -func (f *Functions) BindTexture(target Enum, t Texture) { - f._bindTexture.Invoke(int(target), js.Value(t)) -} - -func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { - panic("not implemented") -} - -func (f *Functions) BindVertexArray(a VertexArray) { - panic("not supported") -} - -func (f *Functions) BlendEquation(mode Enum) { - f._blendEquation.Invoke(int(mode)) -} - -func (f *Functions) BlendFuncSeparate(srcRGB, dstRGB, srcA, dstA Enum) { - f._blendFunc.Invoke(int(srcRGB), int(dstRGB), int(srcA), int(dstA)) -} - -func (f *Functions) BufferData(target Enum, size int, usage Enum, data []byte) { - if data == nil { - f._bufferData.Invoke(int(target), size, int(usage)) - } else { - if len(data) != size { - panic("size mismatch") - } - f._bufferData.Invoke(int(target), f.byteArrayOf(data), int(usage)) - } -} - -func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { - f._bufferSubData.Invoke(int(target), offset, f.byteArrayOf(src)) -} - -func (f *Functions) CheckFramebufferStatus(target Enum) Enum { - status := Enum(f._checkFramebufferStatus.Invoke(int(target)).Int()) - if status != FRAMEBUFFER_COMPLETE && f.Ctx.Call("isContextLost").Bool() { - // If the context is lost, we say that everything is fine. That saves internal/opengl/opengl.go from panic. - return FRAMEBUFFER_COMPLETE - } - return status -} - -func (f *Functions) Clear(mask Enum) { - f._clear.Invoke(int(mask)) -} - -func (f *Functions) ClearColor(red, green, blue, alpha float32) { - f._clearColor.Invoke(red, green, blue, alpha) -} - -func (f *Functions) ClearDepthf(d float32) { - f._clearDepth.Invoke(d) -} - -func (f *Functions) CompileShader(s Shader) { - f._compileShader.Invoke(js.Value(s)) -} - -func (f *Functions) CopyTexSubImage2D(target Enum, level, xoffset, yoffset, x, y, width, height int) { - f._copyTexSubImage2D.Invoke(int(target), level, xoffset, yoffset, x, y, width, height) -} - -func (f *Functions) CreateBuffer() Buffer { - return Buffer(f._createBuffer.Invoke()) -} - -func (f *Functions) CreateFramebuffer() Framebuffer { - return Framebuffer(f._createFramebuffer.Invoke()) -} - -func (f *Functions) CreateProgram() Program { - return Program(f._createProgram.Invoke()) -} - -func (f *Functions) CreateQuery() Query { - return Query(f._createQuery.Invoke()) -} - -func (f *Functions) CreateRenderbuffer() Renderbuffer { - return Renderbuffer(f._createRenderbuffer.Invoke()) -} - -func (f *Functions) CreateShader(ty Enum) Shader { - return Shader(f._createShader.Invoke(int(ty))) -} - -func (f *Functions) CreateTexture() Texture { - return Texture(f._createTexture.Invoke()) -} - -func (f *Functions) CreateVertexArray() VertexArray { - panic("not supported") -} - -func (f *Functions) DeleteBuffer(v Buffer) { - f._deleteBuffer.Invoke(js.Value(v)) -} - -func (f *Functions) DeleteFramebuffer(v Framebuffer) { - f._deleteFramebuffer.Invoke(js.Value(v)) -} - -func (f *Functions) DeleteProgram(p Program) { - f._deleteProgram.Invoke(js.Value(p)) -} - -func (f *Functions) DeleteQuery(query Query) { - if !f.EXT_disjoint_timer_query_webgl2.IsNull() { - f._deleteQuery.Invoke(js.Value(query)) - } else { - f.EXT_disjoint_timer_query.Call("deleteQueryEXT", js.Value(query)) - } -} - -func (f *Functions) DeleteShader(s Shader) { - f._deleteShader.Invoke(js.Value(s)) -} - -func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { - f._deleteRenderbuffer.Invoke(js.Value(v)) -} - -func (f *Functions) DeleteTexture(v Texture) { - f._deleteTexture.Invoke(js.Value(v)) -} - -func (f *Functions) DeleteVertexArray(a VertexArray) { - panic("not implemented") -} - -func (f *Functions) DepthFunc(fn Enum) { - f._depthFunc.Invoke(int(fn)) -} - -func (f *Functions) DepthMask(mask bool) { - f._depthMask.Invoke(mask) -} - -func (f *Functions) DisableVertexAttribArray(a Attrib) { - f._disableVertexAttribArray.Invoke(int(a)) -} - -func (f *Functions) Disable(cap Enum) { - f._disable.Invoke(int(cap)) -} - -func (f *Functions) DrawArrays(mode Enum, first, count int) { - f._drawArrays.Invoke(int(mode), first, count) -} - -func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { - f._drawElements.Invoke(int(mode), count, int(ty), offset) -} - -func (f *Functions) DispatchCompute(x, y, z int) { - panic("not implemented") -} - -func (f *Functions) Enable(cap Enum) { - f._enable.Invoke(int(cap)) -} - -func (f *Functions) EnableVertexAttribArray(a Attrib) { - f._enableVertexAttribArray.Invoke(int(a)) -} - -func (f *Functions) EndQuery(target Enum) { - if !f.EXT_disjoint_timer_query_webgl2.IsNull() { - f._endQuery.Invoke(int(target)) - } else { - f.EXT_disjoint_timer_query.Call("endQueryEXT", int(target)) - } -} - -func (f *Functions) Finish() { - f._finish.Invoke() -} - -func (f *Functions) Flush() { - f._flush.Invoke() -} - -func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { - f._framebufferRenderbuffer.Invoke(int(target), int(attachment), int(renderbuffertarget), js.Value(renderbuffer)) -} - -func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { - f._framebufferTexture2D.Invoke(int(target), int(attachment), int(texTarget), js.Value(t), level) -} - -func (f *Functions) GenerateMipmap(target Enum) { - f._generateMipmap.Invoke(int(target)) -} - -func (f *Functions) GetError() Enum { - // Avoid slow getError calls. See gio#179. - return 0 -} - -func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { - return paramVal(f._getRenderbufferParameteri.Invoke(int(pname))) -} - -func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { - if !f.isWebGL2 && pname == FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING { - // FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING is only available on WebGL 2 - return LINEAR - } - return paramVal(f._getFramebufferAttachmentParameter.Invoke(int(target), int(attachment), int(pname))) -} - -func (f *Functions) GetBinding(pname Enum) Object { - obj := f._getParameter.Invoke(int(pname)) - if !obj.Truthy() { - return Object{} - } - return Object(obj) -} - -func (f *Functions) GetBindingi(pname Enum, idx int) Object { - obj := f._getIndexedParameter.Invoke(int(pname), idx) - if !obj.Truthy() { - return Object{} - } - return Object(obj) -} - -func (f *Functions) GetInteger(pname Enum) int { - if !f.isWebGL2 { - switch pname { - case PACK_ROW_LENGTH, UNPACK_ROW_LENGTH: - return 0 // PACK_ROW_LENGTH and UNPACK_ROW_LENGTH is only available on WebGL 2 - } - } - return paramVal(f._getParameter.Invoke(int(pname))) -} - -func (f *Functions) GetFloat(pname Enum) float32 { - return float32(f._getParameter.Invoke(int(pname)).Float()) -} - -func (f *Functions) GetInteger4(pname Enum) [4]int { - arr := f._getParameter.Invoke(int(pname)) - var res [4]int - for i := range res { - res[i] = arr.Index(i).Int() - } - return res -} - -func (f *Functions) GetFloat4(pname Enum) [4]float32 { - arr := f._getParameter.Invoke(int(pname)) - var res [4]float32 - for i := range res { - res[i] = float32(arr.Index(i).Float()) - } - return res -} - -func (f *Functions) GetProgrami(p Program, pname Enum) int { - return paramVal(f._getProgramParameter.Invoke(js.Value(p), int(pname))) -} - -func (f *Functions) GetProgramInfoLog(p Program) string { - return f._getProgramInfoLog.Invoke(js.Value(p)).String() -} - -func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { - if !f.EXT_disjoint_timer_query_webgl2.IsNull() { - return uint(paramVal(f._getQueryParameter.Invoke(js.Value(query), int(pname)))) - } else { - return uint(paramVal(f.EXT_disjoint_timer_query.Call("getQueryObjectEXT", js.Value(query), int(pname)))) - } -} - -func (f *Functions) GetShaderi(s Shader, pname Enum) int { - return paramVal(f._getShaderParameter.Invoke(js.Value(s), int(pname))) -} - -func (f *Functions) GetShaderInfoLog(s Shader) string { - return f._getShaderInfoLog.Invoke(js.Value(s)).String() -} - -func (f *Functions) GetString(pname Enum) string { - switch pname { - case EXTENSIONS: - extsjs := f._getSupportedExtensions.Invoke() - var exts []string - for i := 0; i < extsjs.Length(); i++ { - exts = append(exts, "GL_"+extsjs.Index(i).String()) - } - return strings.Join(exts, " ") - default: - return f._getParameter.Invoke(int(pname)).String() - } -} - -func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { - return uint(paramVal(f._getUniformBlockIndex.Invoke(js.Value(p), name))) -} - -func (f *Functions) GetUniformLocation(p Program, name string) Uniform { - return Uniform(f._getUniformLocation.Invoke(js.Value(p), name)) -} - -func (f *Functions) GetVertexAttrib(index int, pname Enum) int { - return paramVal(f._getVertexAttrib.Invoke(index, int(pname))) -} - -func (f *Functions) GetVertexAttribBinding(index int, pname Enum) Object { - obj := f._getVertexAttrib.Invoke(index, int(pname)) - if !obj.Truthy() { - return Object{} - } - return Object(obj) -} - -func (f *Functions) GetVertexAttribPointer(index int, pname Enum) uintptr { - return uintptr(f._getVertexAttribOffset.Invoke(index, int(pname)).Int()) -} - -func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { - fn := f.Ctx.Get("invalidateFramebuffer") - if !fn.IsUndefined() { - if f.int32Buf.IsUndefined() { - f.int32Buf = js.Global().Get("Int32Array").New(1) - } - f.int32Buf.SetIndex(0, int32(attachment)) - f._invalidateFramebuffer.Invoke(int(target), f.int32Buf) - } -} - -func (f *Functions) IsEnabled(cap Enum) bool { - return f._isEnabled.Invoke(int(cap)).Truthy() -} - -func (f *Functions) LinkProgram(p Program) { - f._linkProgram.Invoke(js.Value(p)) -} - -func (f *Functions) PixelStorei(pname Enum, param int) { - f._pixelStorei.Invoke(int(pname), param) -} - -func (f *Functions) MemoryBarrier(barriers Enum) { - panic("not implemented") -} - -func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { - panic("not implemented") -} - -func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { - f._renderbufferStorage.Invoke(int(target), int(internalformat), width, height) -} - -func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { - ba := f.byteArrayOf(data) - f._readPixels.Invoke(x, y, width, height, int(format), int(ty), ba) - js.CopyBytesToGo(data, ba) -} - -func (f *Functions) Scissor(x, y, width, height int32) { - f._scissor.Invoke(x, y, width, height) -} - -func (f *Functions) ShaderSource(s Shader, src string) { - f._shaderSource.Invoke(js.Value(s), src) -} - -func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width, height int, format, ty Enum) { - f._texImage2D.Invoke(int(target), int(level), int(internalFormat), int(width), int(height), 0, int(format), int(ty), nil) -} - -func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { - f._texStorage2D.Invoke(int(target), levels, int(internalFormat), width, height) -} - -func (f *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { - f._texSubImage2D.Invoke(int(target), level, x, y, width, height, int(format), int(ty), f.byteArrayOf(data)) -} - -func (f *Functions) TexParameteri(target, pname Enum, param int) { - f._texParameteri.Invoke(int(target), int(pname), int(param)) -} - -func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { - f._uniformBlockBinding.Invoke(js.Value(p), int(uniformBlockIndex), int(uniformBlockBinding)) -} - -func (f *Functions) Uniform1f(dst Uniform, v float32) { - f._uniform1f.Invoke(js.Value(dst), v) -} - -func (f *Functions) Uniform1i(dst Uniform, v int) { - f._uniform1i.Invoke(js.Value(dst), v) -} - -func (f *Functions) Uniform2f(dst Uniform, v0, v1 float32) { - f._uniform2f.Invoke(js.Value(dst), v0, v1) -} - -func (f *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { - f._uniform3f.Invoke(js.Value(dst), v0, v1, v2) -} - -func (f *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { - f._uniform4f.Invoke(js.Value(dst), v0, v1, v2, v3) -} - -func (f *Functions) UseProgram(p Program) { - f._useProgram.Invoke(js.Value(p)) -} - -func (f *Functions) UnmapBuffer(target Enum) bool { - panic("not implemented") -} - -func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { - f._vertexAttribPointer.Invoke(int(dst), size, int(ty), normalized, stride, offset) -} - -func (f *Functions) Viewport(x, y, width, height int) { - f._viewport.Invoke(x, y, width, height) -} - -func (f *Functions) byteArrayOf(data []byte) js.Value { - if len(data) == 0 { - return js.Null() - } - f.resizeByteBuffer(len(data)) - ba := f.uint8Array.New(f.arrayBuf, int(0), int(len(data))) - js.CopyBytesToJS(ba, data) - return ba -} - -func (f *Functions) resizeByteBuffer(n int) { - if n == 0 { - return - } - if !f.arrayBuf.IsUndefined() && f.arrayBuf.Length() >= n { - return - } - f.arrayBuf = js.Global().Get("ArrayBuffer").New(n) -} - -func paramVal(v js.Value) int { - switch v.Type() { - case js.TypeBoolean: - if b := v.Bool(); b { - return 1 - } else { - return 0 - } - case js.TypeNumber: - return v.Int() - case js.TypeUndefined: - return 0 - case js.TypeNull: - return 0 - default: - panic("unknown parameter type") - } -} diff --git a/gio/internal/gl/gl_unix.go b/gio/internal/gl/gl_unix.go deleted file mode 100644 index a1de57e..0000000 --- a/gio/internal/gl/gl_unix.go +++ /dev/null @@ -1,1323 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build darwin || linux || freebsd || openbsd -// +build darwin linux freebsd openbsd - -package gl - -import ( - "fmt" - "runtime" - "strings" - "unsafe" -) - -/* -#cgo CFLAGS: -Werror -#cgo linux freebsd LDFLAGS: -ldl - -#include -#include -#include -#define __USE_GNU -#include - -typedef unsigned int GLenum; -typedef unsigned int GLuint; -typedef char GLchar; -typedef float GLfloat; -typedef ssize_t GLsizeiptr; -typedef intptr_t GLintptr; -typedef unsigned int GLbitfield; -typedef int GLint; -typedef unsigned char GLboolean; -typedef int GLsizei; -typedef uint8_t GLubyte; - -typedef void (*_glActiveTexture)(GLenum texture); -typedef void (*_glAttachShader)(GLuint program, GLuint shader); -typedef void (*_glBindAttribLocation)(GLuint program, GLuint index, const GLchar *name); -typedef void (*_glBindBuffer)(GLenum target, GLuint buffer); -typedef void (*_glBindFramebuffer)(GLenum target, GLuint framebuffer); -typedef void (*_glBindRenderbuffer)(GLenum target, GLuint renderbuffer); -typedef void (*_glBindTexture)(GLenum target, GLuint texture); -typedef void (*_glBlendEquation)(GLenum mode); -typedef void (*_glBlendFuncSeparate)(GLenum srcRGB, GLenum dstRGB, GLenum srcA, GLenum dstA); -typedef void (*_glBufferData)(GLenum target, GLsizeiptr size, const void *data, GLenum usage); -typedef void (*_glBufferSubData)(GLenum target, GLintptr offset, GLsizeiptr size, const void *data); -typedef GLenum (*_glCheckFramebufferStatus)(GLenum target); -typedef void (*_glClear)(GLbitfield mask); -typedef void (*_glClearColor)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); -typedef void (*_glClearDepthf)(GLfloat d); -typedef void (*_glCompileShader)(GLuint shader); -typedef void (*_glCopyTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height); -typedef GLuint (*_glCreateProgram)(void); -typedef GLuint (*_glCreateShader)(GLenum type); -typedef void (*_glDeleteBuffers)(GLsizei n, const GLuint *buffers); -typedef void (*_glDeleteFramebuffers)(GLsizei n, const GLuint *framebuffers); -typedef void (*_glDeleteProgram)(GLuint program); -typedef void (*_glDeleteRenderbuffers)(GLsizei n, const GLuint *renderbuffers); -typedef void (*_glDeleteShader)(GLuint shader); -typedef void (*_glDeleteTextures)(GLsizei n, const GLuint *textures); -typedef void (*_glDepthFunc)(GLenum func); -typedef void (*_glDepthMask)(GLboolean flag); -typedef void (*_glDisable)(GLenum cap); -typedef void (*_glDisableVertexAttribArray)(GLuint index); -typedef void (*_glDrawArrays)(GLenum mode, GLint first, GLsizei count); -typedef void (*_glDrawElements)(GLenum mode, GLsizei count, GLenum type, const void *indices); -typedef void (*_glEnable)(GLenum cap); -typedef void (*_glEnableVertexAttribArray)(GLuint index); -typedef void (*_glFinish)(void); -typedef void (*_glFlush)(void); -typedef void (*_glFramebufferRenderbuffer)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); -typedef void (*_glFramebufferTexture2D)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); -typedef void (*_glGenBuffers)(GLsizei n, GLuint *buffers); -typedef void (*_glGenerateMipmap)(GLenum target); -typedef void (*_glGenFramebuffers)(GLsizei n, GLuint *framebuffers); -typedef void (*_glGenRenderbuffers)(GLsizei n, GLuint *renderbuffers); -typedef void (*_glGenTextures)(GLsizei n, GLuint *textures); -typedef GLenum (*_glGetError)(void); -typedef void (*_glGetFramebufferAttachmentParameteriv)(GLenum target, GLenum attachment, GLenum pname, GLint *params); -typedef void (*_glGetFloatv)(GLenum pname, GLfloat *data); -typedef void (*_glGetIntegerv)(GLenum pname, GLint *data); -typedef void (*_glGetIntegeri_v)(GLenum pname, GLuint idx, GLint *data); -typedef void (*_glGetProgramiv)(GLuint program, GLenum pname, GLint *params); -typedef void (*_glGetProgramInfoLog)(GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog); -typedef void (*_glGetRenderbufferParameteriv)(GLenum target, GLenum pname, GLint *params); -typedef void (*_glGetShaderiv)(GLuint shader, GLenum pname, GLint *params); -typedef void (*_glGetShaderInfoLog)(GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog); -typedef const GLubyte *(*_glGetString)(GLenum name); -typedef GLint (*_glGetUniformLocation)(GLuint program, const GLchar *name); -typedef void (*_glGetVertexAttribiv)(GLuint index, GLenum pname, GLint *params); -typedef void (*_glGetVertexAttribPointerv)(GLuint index, GLenum pname, void **params); -typedef GLboolean (*_glIsEnabled)(GLenum cap); -typedef void (*_glLinkProgram)(GLuint program); -typedef void (*_glPixelStorei)(GLenum pname, GLint param); -typedef void (*_glReadPixels)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels); -typedef void (*_glRenderbufferStorage)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height); -typedef void (*_glScissor)(GLint x, GLint y, GLsizei width, GLsizei height); -typedef void (*_glShaderSource)(GLuint shader, GLsizei count, const GLchar *const*string, const GLint *length); -typedef void (*_glTexImage2D)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels); -typedef void (*_glTexParameteri)(GLenum target, GLenum pname, GLint param); -typedef void (*_glTexSubImage2D)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels); -typedef void (*_glUniform1f)(GLint location, GLfloat v0); -typedef void (*_glUniform1i)(GLint location, GLint v0); -typedef void (*_glUniform2f)(GLint location, GLfloat v0, GLfloat v1); -typedef void (*_glUniform3f)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2); -typedef void (*_glUniform4f)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3); -typedef void (*_glUseProgram)(GLuint program); -typedef void (*_glVertexAttribPointer)(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void *pointer); -typedef void (*_glViewport)(GLint x, GLint y, GLsizei width, GLsizei height); -typedef void (*_glBindVertexArray)(GLuint array); -typedef void (*_glBindBufferBase)(GLenum target, GLuint index, GLuint buffer); -typedef GLuint (*_glGetUniformBlockIndex)(GLuint program, const GLchar *uniformBlockName); -typedef void (*_glUniformBlockBinding)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding); -typedef void (*_glInvalidateFramebuffer)(GLenum target, GLsizei numAttachments, const GLenum *attachments); -typedef void (*_glBeginQuery)(GLenum target, GLuint id); -typedef void (*_glDeleteQueries)(GLsizei n, const GLuint *ids); -typedef void (*_glDeleteVertexArrays)(GLsizei n, const GLuint *ids); -typedef void (*_glEndQuery)(GLenum target); -typedef void (*_glGenQueries)(GLsizei n, GLuint *ids); -typedef void (*_glGenVertexArrays)(GLsizei n, GLuint *ids); -typedef void (*_glGetProgramBinary)(GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary); -typedef void (*_glGetQueryObjectuiv)(GLuint id, GLenum pname, GLuint *params); -typedef const GLubyte* (*_glGetStringi)(GLenum name, GLuint index); -typedef void (*_glDispatchCompute)(GLuint x, GLuint y, GLuint z); -typedef void (*_glMemoryBarrier)(GLbitfield barriers); -typedef void* (*_glMapBufferRange)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access); -typedef GLboolean (*_glUnmapBuffer)(GLenum target); -typedef void (*_glBindImageTexture)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format); -typedef void (*_glTexStorage2D)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); -typedef void (*_glBlitFramebuffer)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter); - -static void glActiveTexture(_glActiveTexture f, GLenum texture) { - f(texture); -} - -static void glAttachShader(_glAttachShader f, GLuint program, GLuint shader) { - f(program, shader); -} - -static void glBindAttribLocation(_glBindAttribLocation f, GLuint program, GLuint index, const GLchar *name) { - f(program, index, name); -} - -static void glBindBuffer(_glBindBuffer f, GLenum target, GLuint buffer) { - f(target, buffer); -} - -static void glBindFramebuffer(_glBindFramebuffer f, GLenum target, GLuint framebuffer) { - f(target, framebuffer); -} - -static void glBindRenderbuffer(_glBindRenderbuffer f, GLenum target, GLuint renderbuffer) { - f(target, renderbuffer); -} - -static void glBindTexture(_glBindTexture f, GLenum target, GLuint texture) { - f(target, texture); -} - -static void glBindVertexArray(_glBindVertexArray f, GLuint array) { - f(array); -} - -static void glBlendEquation(_glBlendEquation f, GLenum mode) { - f(mode); -} - -static void glBlendFuncSeparate(_glBlendFuncSeparate f, GLenum srcRGB, GLenum dstRGB, GLenum srcA, GLenum dstA) { - f(srcRGB, dstRGB, srcA, dstA); -} - -static void glBufferData(_glBufferData f, GLenum target, GLsizeiptr size, const void *data, GLenum usage) { - f(target, size, data, usage); -} - -static void glBufferSubData(_glBufferSubData f, GLenum target, GLintptr offset, GLsizeiptr size, const void *data) { - f(target, offset, size, data); -} - -static GLenum glCheckFramebufferStatus(_glCheckFramebufferStatus f, GLenum target) { - return f(target); -} - -static void glClear(_glClear f, GLbitfield mask) { - f(mask); -} - -static void glClearColor(_glClearColor f, GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha) { - f(red, green, blue, alpha); -} - -static void glClearDepthf(_glClearDepthf f, GLfloat d) { - f(d); -} - -static void glCompileShader(_glCompileShader f, GLuint shader) { - f(shader); -} - -static void glCopyTexSubImage2D(_glCopyTexSubImage2D f, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height) { - f(target, level, xoffset, yoffset, x, y, width, height); -} - -static GLuint glCreateProgram(_glCreateProgram f) { - return f(); -} - -static GLuint glCreateShader(_glCreateShader f, GLenum type) { - return f(type); -} - -static void glDeleteBuffers(_glDeleteBuffers f, GLsizei n, const GLuint *buffers) { - f(n, buffers); -} - -static void glDeleteFramebuffers(_glDeleteFramebuffers f, GLsizei n, const GLuint *framebuffers) { - f(n, framebuffers); -} - -static void glDeleteProgram(_glDeleteProgram f, GLuint program) { - f(program); -} - -static void glDeleteRenderbuffers(_glDeleteRenderbuffers f, GLsizei n, const GLuint *renderbuffers) { - f(n, renderbuffers); -} - -static void glDeleteShader(_glDeleteShader f, GLuint shader) { - f(shader); -} - -static void glDeleteTextures(_glDeleteTextures f, GLsizei n, const GLuint *textures) { - f(n, textures); -} - -static void glDepthFunc(_glDepthFunc f, GLenum func) { - f(func); -} - -static void glDepthMask(_glDepthMask f, GLboolean flag) { - f(flag); -} - -static void glDisable(_glDisable f, GLenum cap) { - f(cap); -} - -static void glDisableVertexAttribArray(_glDisableVertexAttribArray f, GLuint index) { - f(index); -} - -static void glDrawArrays(_glDrawArrays f, GLenum mode, GLint first, GLsizei count) { - f(mode, first, count); -} - -// offset is defined as an uintptr_t to omit Cgo pointer checks. -static void glDrawElements(_glDrawElements f, GLenum mode, GLsizei count, GLenum type, const uintptr_t offset) { - f(mode, count, type, (const void *)offset); -} - -static void glEnable(_glEnable f, GLenum cap) { - f(cap); -} - -static void glEnableVertexAttribArray(_glEnableVertexAttribArray f, GLuint index) { - f(index); -} - -static void glFinish(_glFinish f) { - f(); -} - -static void glFlush(_glFlush f) { - f(); -} - -static void glFramebufferRenderbuffer(_glFramebufferRenderbuffer f, GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer) { - f(target, attachment, renderbuffertarget, renderbuffer); -} - -static void glFramebufferTexture2D(_glFramebufferTexture2D f, GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) { - f(target, attachment, textarget, texture, level); -} - -static void glGenBuffers(_glGenBuffers f, GLsizei n, GLuint *buffers) { - f(n, buffers); -} - -static void glGenerateMipmap(_glGenerateMipmap f, GLenum target) { - f(target); -} - -static void glGenFramebuffers(_glGenFramebuffers f, GLsizei n, GLuint *framebuffers) { - f(n, framebuffers); -} - -static void glGenRenderbuffers(_glGenRenderbuffers f, GLsizei n, GLuint *renderbuffers) { - f(n, renderbuffers); -} - -static void glGenTextures(_glGenTextures f, GLsizei n, GLuint *textures) { - f(n, textures); -} - -static GLenum glGetError(_glGetError f) { - return f(); -} - -static void glGetFramebufferAttachmentParameteriv(_glGetFramebufferAttachmentParameteriv f, GLenum target, GLenum attachment, GLenum pname, GLint *params) { - f(target, attachment, pname, params); -} - -static void glGetIntegerv(_glGetIntegerv f, GLenum pname, GLint *data) { - f(pname, data); -} - -static void glGetFloatv(_glGetFloatv f, GLenum pname, GLfloat *data) { - f(pname, data); -} - -static void glGetIntegeri_v(_glGetIntegeri_v f, GLenum pname, GLuint idx, GLint *data) { - f(pname, idx, data); -} - -static void glGetProgramiv(_glGetProgramiv f, GLuint program, GLenum pname, GLint *params) { - f(program, pname, params); -} - -static void glGetProgramInfoLog(_glGetProgramInfoLog f, GLuint program, GLsizei bufSize, GLsizei *length, GLchar *infoLog) { - f(program, bufSize, length, infoLog); -} - -static void glGetRenderbufferParameteriv(_glGetRenderbufferParameteriv f, GLenum target, GLenum pname, GLint *params) { - f(target, pname, params); -} - -static void glGetShaderiv(_glGetShaderiv f, GLuint shader, GLenum pname, GLint *params) { - f(shader, pname, params); -} - -static void glGetShaderInfoLog(_glGetShaderInfoLog f, GLuint shader, GLsizei bufSize, GLsizei *length, GLchar *infoLog) { - f(shader, bufSize, length, infoLog); -} - -static const GLubyte *glGetString(_glGetString f, GLenum name) { - return f(name); -} - -static GLint glGetUniformLocation(_glGetUniformLocation f, GLuint program, const GLchar *name) { - return f(program, name); -} - -static void glGetVertexAttribiv(_glGetVertexAttribiv f, GLuint index, GLenum pname, GLint *data) { - f(index, pname, data); -} - -// Return uintptr_t to avoid Cgo pointer check. -static uintptr_t glGetVertexAttribPointerv(_glGetVertexAttribPointerv f, GLuint index, GLenum pname) { - void *ptrs; - f(index, pname, &ptrs); - return (uintptr_t)ptrs; -} - -static GLboolean glIsEnabled(_glIsEnabled f, GLenum cap) { - return f(cap); -} - -static void glLinkProgram(_glLinkProgram f, GLuint program) { - f(program); -} - -static void glPixelStorei(_glPixelStorei f, GLenum pname, GLint param) { - f(pname, param); -} - -static void glReadPixels(_glReadPixels f, GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void *pixels) { - f(x, y, width, height, format, type, pixels); -} - -static void glRenderbufferStorage(_glRenderbufferStorage f, GLenum target, GLenum internalformat, GLsizei width, GLsizei height) { - f(target, internalformat, width, height); -} - -static void glScissor(_glScissor f, GLint x, GLint y, GLsizei width, GLsizei height) { - f(x, y, width, height); -} - -static void glShaderSource(_glShaderSource f, GLuint shader, GLsizei count, const GLchar *const*string, const GLint *length) { - f(shader, count, string, length); -} - -static void glTexImage2D(_glTexImage2D f, GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *pixels) { - f(target, level, internalformat, width, height, border, format, type, pixels); -} - -static void glTexParameteri(_glTexParameteri f, GLenum target, GLenum pname, GLint param) { - f(target, pname, param); -} - -static void glTexSubImage2D(_glTexSubImage2D f, GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void *pixels) { - f(target, level, xoffset, yoffset, width, height, format, type, pixels); -} - -static void glUniform1f(_glUniform1f f, GLint location, GLfloat v0) { - f(location, v0); -} - -static void glUniform1i(_glUniform1i f, GLint location, GLint v0) { - f(location, v0); -} - -static void glUniform2f(_glUniform2f f, GLint location, GLfloat v0, GLfloat v1) { - f(location, v0, v1); -} - -static void glUniform3f(_glUniform3f f, GLint location, GLfloat v0, GLfloat v1, GLfloat v2) { - f(location, v0, v1, v2); -} - -static void glUniform4f(_glUniform4f f, GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3) { - f(location, v0, v1, v2, v3); -} - -static void glUseProgram(_glUseProgram f, GLuint program) { - f(program); -} - -// offset is defined as an uintptr_t to omit Cgo pointer checks. -static void glVertexAttribPointer(_glVertexAttribPointer f, GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, uintptr_t offset) { - f(index, size, type, normalized, stride, (const void *)offset); -} - -static void glViewport(_glViewport f, GLint x, GLint y, GLsizei width, GLsizei height) { - f(x, y, width, height); -} - -static void glBindBufferBase(_glBindBufferBase f, GLenum target, GLuint index, GLuint buffer) { - f(target, index, buffer); -} - -static void glUniformBlockBinding(_glUniformBlockBinding f, GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding) { - f(program, uniformBlockIndex, uniformBlockBinding); -} - -static GLuint glGetUniformBlockIndex(_glGetUniformBlockIndex f, GLuint program, const GLchar *uniformBlockName) { - return f(program, uniformBlockName); -} - -static void glInvalidateFramebuffer(_glInvalidateFramebuffer f, GLenum target, GLenum attachment) { - // Framebuffer invalidation is just a hint and can safely be ignored. - if (f != NULL) { - f(target, 1, &attachment); - } -} - -static void glBeginQuery(_glBeginQuery f, GLenum target, GLenum attachment) { - f(target, attachment); -} - -static void glDeleteQueries(_glDeleteQueries f, GLsizei n, const GLuint *ids) { - f(n, ids); -} - -static void glDeleteVertexArrays(_glDeleteVertexArrays f, GLsizei n, const GLuint *ids) { - f(n, ids); -} - -static void glEndQuery(_glEndQuery f, GLenum target) { - f(target); -} - -static const GLubyte* glGetStringi(_glGetStringi f, GLenum name, GLuint index) { - return f(name, index); -} - -static void glGenQueries(_glGenQueries f, GLsizei n, GLuint *ids) { - f(n, ids); -} - -static void glGenVertexArrays(_glGenVertexArrays f, GLsizei n, GLuint *ids) { - f(n, ids); -} - -static void glGetProgramBinary(_glGetProgramBinary f, GLuint program, GLsizei bufsize, GLsizei *length, GLenum *binaryFormat, void *binary) { - f(program, bufsize, length, binaryFormat, binary); -} - -static void glGetQueryObjectuiv(_glGetQueryObjectuiv f, GLuint id, GLenum pname, GLuint *params) { - f(id, pname, params); -} - -static void glMemoryBarrier(_glMemoryBarrier f, GLbitfield barriers) { - f(barriers); -} - -static void glDispatchCompute(_glDispatchCompute f, GLuint x, GLuint y, GLuint z) { - f(x, y, z); -} - -static void *glMapBufferRange(_glMapBufferRange f, GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access) { - return f(target, offset, length, access); -} - -static GLboolean glUnmapBuffer(_glUnmapBuffer f, GLenum target) { - return f(target); -} - -static void glBindImageTexture(_glBindImageTexture f, GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format) { - f(unit, texture, level, layered, layer, access, format); -} - -static void glTexStorage2D(_glTexStorage2D f, GLenum target, GLsizei levels, GLenum internalFormat, GLsizei width, GLsizei height) { - f(target, levels, internalFormat, width, height); -} - -static void glBlitFramebuffer(_glBlitFramebuffer f, GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter) { - f(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, mask, filter); -} -*/ -import "C" - -type Context interface{} - -type Functions struct { - // Query caches. - uints [100]C.GLuint - ints [100]C.GLint - floats [100]C.GLfloat - - glActiveTexture C._glActiveTexture - glAttachShader C._glAttachShader - glBindAttribLocation C._glBindAttribLocation - glBindBuffer C._glBindBuffer - glBindFramebuffer C._glBindFramebuffer - glBindRenderbuffer C._glBindRenderbuffer - glBindTexture C._glBindTexture - glBlendEquation C._glBlendEquation - glBlendFuncSeparate C._glBlendFuncSeparate - glBufferData C._glBufferData - glBufferSubData C._glBufferSubData - glCheckFramebufferStatus C._glCheckFramebufferStatus - glClear C._glClear - glClearColor C._glClearColor - glClearDepthf C._glClearDepthf - glCompileShader C._glCompileShader - glCopyTexSubImage2D C._glCopyTexSubImage2D - glCreateProgram C._glCreateProgram - glCreateShader C._glCreateShader - glDeleteBuffers C._glDeleteBuffers - glDeleteFramebuffers C._glDeleteFramebuffers - glDeleteProgram C._glDeleteProgram - glDeleteRenderbuffers C._glDeleteRenderbuffers - glDeleteShader C._glDeleteShader - glDeleteTextures C._glDeleteTextures - glDepthFunc C._glDepthFunc - glDepthMask C._glDepthMask - glDisable C._glDisable - glDisableVertexAttribArray C._glDisableVertexAttribArray - glDrawArrays C._glDrawArrays - glDrawElements C._glDrawElements - glEnable C._glEnable - glEnableVertexAttribArray C._glEnableVertexAttribArray - glFinish C._glFinish - glFlush C._glFlush - glFramebufferRenderbuffer C._glFramebufferRenderbuffer - glFramebufferTexture2D C._glFramebufferTexture2D - glGenBuffers C._glGenBuffers - glGenerateMipmap C._glGenerateMipmap - glGenFramebuffers C._glGenFramebuffers - glGenRenderbuffers C._glGenRenderbuffers - glGenTextures C._glGenTextures - glGetError C._glGetError - glGetFramebufferAttachmentParameteriv C._glGetFramebufferAttachmentParameteriv - glGetFloatv C._glGetFloatv - glGetIntegerv C._glGetIntegerv - glGetIntegeri_v C._glGetIntegeri_v - glGetProgramiv C._glGetProgramiv - glGetProgramInfoLog C._glGetProgramInfoLog - glGetRenderbufferParameteriv C._glGetRenderbufferParameteriv - glGetShaderiv C._glGetShaderiv - glGetShaderInfoLog C._glGetShaderInfoLog - glGetString C._glGetString - glGetUniformLocation C._glGetUniformLocation - glGetVertexAttribiv C._glGetVertexAttribiv - glGetVertexAttribPointerv C._glGetVertexAttribPointerv - glIsEnabled C._glIsEnabled - glLinkProgram C._glLinkProgram - glPixelStorei C._glPixelStorei - glReadPixels C._glReadPixels - glRenderbufferStorage C._glRenderbufferStorage - glScissor C._glScissor - glShaderSource C._glShaderSource - glTexImage2D C._glTexImage2D - glTexParameteri C._glTexParameteri - glTexSubImage2D C._glTexSubImage2D - glUniform1f C._glUniform1f - glUniform1i C._glUniform1i - glUniform2f C._glUniform2f - glUniform3f C._glUniform3f - glUniform4f C._glUniform4f - glUseProgram C._glUseProgram - glVertexAttribPointer C._glVertexAttribPointer - glViewport C._glViewport - glBindVertexArray C._glBindVertexArray - glBindBufferBase C._glBindBufferBase - glGetUniformBlockIndex C._glGetUniformBlockIndex - glUniformBlockBinding C._glUniformBlockBinding - glInvalidateFramebuffer C._glInvalidateFramebuffer - glBeginQuery C._glBeginQuery - glDeleteQueries C._glDeleteQueries - glDeleteVertexArrays C._glDeleteVertexArrays - glEndQuery C._glEndQuery - glGenQueries C._glGenQueries - glGenVertexArrays C._glGenVertexArrays - glGetProgramBinary C._glGetProgramBinary - glGetQueryObjectuiv C._glGetQueryObjectuiv - glGetStringi C._glGetStringi - glDispatchCompute C._glDispatchCompute - glMemoryBarrier C._glMemoryBarrier - glMapBufferRange C._glMapBufferRange - glUnmapBuffer C._glUnmapBuffer - glBindImageTexture C._glBindImageTexture - glTexStorage2D C._glTexStorage2D - glBlitFramebuffer C._glBlitFramebuffer -} - -func NewFunctions(ctx Context, forceES bool) (*Functions, error) { - if ctx != nil { - panic("non-nil context") - } - f := new(Functions) - err := f.load(forceES) - if err != nil { - return nil, err - } - return f, nil -} - -func dlsym(handle unsafe.Pointer, s string) unsafe.Pointer { - cs := C.CString(s) - defer C.free(unsafe.Pointer(cs)) - return C.dlsym(handle, cs) -} - -func dlopen(lib string) unsafe.Pointer { - clib := C.CString(lib) - defer C.free(unsafe.Pointer(clib)) - return C.dlopen(clib, C.RTLD_NOW|C.RTLD_LOCAL) -} - -func (f *Functions) load(forceES bool) error { - var ( - loadErr error - libNames []string - handles []unsafe.Pointer - ) - switch { - case runtime.GOOS == "darwin" && !forceES: - libNames = []string{"/System/Library/Frameworks/OpenGL.framework/OpenGL"} - case runtime.GOOS == "darwin" && forceES: - libNames = []string{"libGLESv2.dylib"} - case runtime.GOOS == "ios": - libNames = []string{"/System/Library/Frameworks/OpenGLES.framework/OpenGLES"} - case runtime.GOOS == "android": - libNames = []string{"libGLESv2.so", "libGLESv3.so"} - default: - libNames = []string{"libGLESv2.so.2", "libGLESv2.so.3.0"} - } - for _, lib := range libNames { - if h := dlopen(lib); h != nil { - handles = append(handles, h) - } - } - if len(handles) == 0 { - return fmt.Errorf("gl: no OpenGL implementation could be loaded (tried %q)", libNames) - } - load := func(s string) *[0]byte { - for _, h := range handles { - if f := dlsym(h, s); f != nil { - return (*[0]byte)(f) - } - } - return nil - } - must := func(s string) *[0]byte { - ptr := load(s) - if ptr == nil { - loadErr = fmt.Errorf("gl: failed to load symbol %q", s) - } - return ptr - } - // GL ES 2.0 functions. - f.glActiveTexture = must("glActiveTexture") - f.glAttachShader = must("glAttachShader") - f.glBindAttribLocation = must("glBindAttribLocation") - f.glBindBuffer = must("glBindBuffer") - f.glBindFramebuffer = must("glBindFramebuffer") - f.glBindRenderbuffer = must("glBindRenderbuffer") - f.glBindTexture = must("glBindTexture") - f.glBlendEquation = must("glBlendEquation") - f.glBlendFuncSeparate = must("glBlendFuncSeparate") - f.glBufferData = must("glBufferData") - f.glBufferSubData = must("glBufferSubData") - f.glCheckFramebufferStatus = must("glCheckFramebufferStatus") - f.glClear = must("glClear") - f.glClearColor = must("glClearColor") - f.glClearDepthf = must("glClearDepthf") - f.glCompileShader = must("glCompileShader") - f.glCopyTexSubImage2D = must("glCopyTexSubImage2D") - f.glCreateProgram = must("glCreateProgram") - f.glCreateShader = must("glCreateShader") - f.glDeleteBuffers = must("glDeleteBuffers") - f.glDeleteFramebuffers = must("glDeleteFramebuffers") - f.glDeleteProgram = must("glDeleteProgram") - f.glDeleteRenderbuffers = must("glDeleteRenderbuffers") - f.glDeleteShader = must("glDeleteShader") - f.glDeleteTextures = must("glDeleteTextures") - f.glDepthFunc = must("glDepthFunc") - f.glDepthMask = must("glDepthMask") - f.glDisable = must("glDisable") - f.glDisableVertexAttribArray = must("glDisableVertexAttribArray") - f.glDrawArrays = must("glDrawArrays") - f.glDrawElements = must("glDrawElements") - f.glEnable = must("glEnable") - f.glEnableVertexAttribArray = must("glEnableVertexAttribArray") - f.glFinish = must("glFinish") - f.glFlush = must("glFlush") - f.glFramebufferRenderbuffer = must("glFramebufferRenderbuffer") - f.glFramebufferTexture2D = must("glFramebufferTexture2D") - f.glGenBuffers = must("glGenBuffers") - f.glGenerateMipmap = must("glGenerateMipmap") - f.glGenFramebuffers = must("glGenFramebuffers") - f.glGenRenderbuffers = must("glGenRenderbuffers") - f.glGenTextures = must("glGenTextures") - f.glGetError = must("glGetError") - f.glGetFramebufferAttachmentParameteriv = must("glGetFramebufferAttachmentParameteriv") - f.glGetIntegerv = must("glGetIntegerv") - f.glGetFloatv = must("glGetFloatv") - f.glGetProgramiv = must("glGetProgramiv") - f.glGetProgramInfoLog = must("glGetProgramInfoLog") - f.glGetRenderbufferParameteriv = must("glGetRenderbufferParameteriv") - f.glGetShaderiv = must("glGetShaderiv") - f.glGetShaderInfoLog = must("glGetShaderInfoLog") - f.glGetString = must("glGetString") - f.glGetUniformLocation = must("glGetUniformLocation") - f.glGetVertexAttribiv = must("glGetVertexAttribiv") - f.glGetVertexAttribPointerv = must("glGetVertexAttribPointerv") - f.glIsEnabled = must("glIsEnabled") - f.glLinkProgram = must("glLinkProgram") - f.glPixelStorei = must("glPixelStorei") - f.glReadPixels = must("glReadPixels") - f.glRenderbufferStorage = must("glRenderbufferStorage") - f.glScissor = must("glScissor") - f.glShaderSource = must("glShaderSource") - f.glTexImage2D = must("glTexImage2D") - f.glTexParameteri = must("glTexParameteri") - f.glTexSubImage2D = must("glTexSubImage2D") - f.glUniform1f = must("glUniform1f") - f.glUniform1i = must("glUniform1i") - f.glUniform2f = must("glUniform2f") - f.glUniform3f = must("glUniform3f") - f.glUniform4f = must("glUniform4f") - f.glUseProgram = must("glUseProgram") - f.glVertexAttribPointer = must("glVertexAttribPointer") - f.glViewport = must("glViewport") - - // Extensions and GL ES 3 functions. - f.glBindBufferBase = load("glBindBufferBase") - f.glBindVertexArray = load("glBindVertexArray") - f.glGetIntegeri_v = load("glGetIntegeri_v") - f.glGetUniformBlockIndex = load("glGetUniformBlockIndex") - f.glUniformBlockBinding = load("glUniformBlockBinding") - f.glInvalidateFramebuffer = load("glInvalidateFramebuffer") - f.glGetStringi = load("glGetStringi") - // Fall back to EXT_invalidate_framebuffer if available. - if f.glInvalidateFramebuffer == nil { - f.glInvalidateFramebuffer = load("glDiscardFramebufferEXT") - } - - f.glBeginQuery = load("glBeginQuery") - if f.glBeginQuery == nil { - f.glBeginQuery = load("glBeginQueryEXT") - } - f.glDeleteQueries = load("glDeleteQueries") - if f.glDeleteQueries == nil { - f.glDeleteQueries = load("glDeleteQueriesEXT") - } - f.glEndQuery = load("glEndQuery") - if f.glEndQuery == nil { - f.glEndQuery = load("glEndQueryEXT") - } - f.glGenQueries = load("glGenQueries") - if f.glGenQueries == nil { - f.glGenQueries = load("glGenQueriesEXT") - } - f.glGetQueryObjectuiv = load("glGetQueryObjectuiv") - if f.glGetQueryObjectuiv == nil { - f.glGetQueryObjectuiv = load("glGetQueryObjectuivEXT") - } - - f.glDeleteVertexArrays = load("glDeleteVertexArrays") - f.glGenVertexArrays = load("glGenVertexArrays") - f.glMemoryBarrier = load("glMemoryBarrier") - f.glDispatchCompute = load("glDispatchCompute") - f.glMapBufferRange = load("glMapBufferRange") - f.glUnmapBuffer = load("glUnmapBuffer") - f.glBindImageTexture = load("glBindImageTexture") - f.glTexStorage2D = load("glTexStorage2D") - f.glBlitFramebuffer = load("glBlitFramebuffer") - f.glGetProgramBinary = load("glGetProgramBinary") - - return loadErr -} - -func (f *Functions) ActiveTexture(texture Enum) { - C.glActiveTexture(f.glActiveTexture, C.GLenum(texture)) -} - -func (f *Functions) AttachShader(p Program, s Shader) { - C.glAttachShader(f.glAttachShader, C.GLuint(p.V), C.GLuint(s.V)) -} - -func (f *Functions) BeginQuery(target Enum, query Query) { - C.glBeginQuery(f.glBeginQuery, C.GLenum(target), C.GLenum(query.V)) -} - -func (f *Functions) BindAttribLocation(p Program, a Attrib, name string) { - cname := C.CString(name) - defer C.free(unsafe.Pointer(cname)) - C.glBindAttribLocation(f.glBindAttribLocation, C.GLuint(p.V), C.GLuint(a), cname) -} - -func (f *Functions) BindBufferBase(target Enum, index int, b Buffer) { - C.glBindBufferBase(f.glBindBufferBase, C.GLenum(target), C.GLuint(index), C.GLuint(b.V)) -} - -func (f *Functions) BindBuffer(target Enum, b Buffer) { - C.glBindBuffer(f.glBindBuffer, C.GLenum(target), C.GLuint(b.V)) -} - -func (f *Functions) BindFramebuffer(target Enum, fb Framebuffer) { - C.glBindFramebuffer(f.glBindFramebuffer, C.GLenum(target), C.GLuint(fb.V)) -} - -func (f *Functions) BindRenderbuffer(target Enum, fb Renderbuffer) { - C.glBindRenderbuffer(f.glBindRenderbuffer, C.GLenum(target), C.GLuint(fb.V)) -} - -func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { - l := C.GLboolean(FALSE) - if layered { - l = TRUE - } - C.glBindImageTexture(f.glBindImageTexture, C.GLuint(unit), C.GLuint(t.V), C.GLint(level), l, C.GLint(layer), C.GLenum(access), C.GLenum(format)) -} - -func (f *Functions) BindTexture(target Enum, t Texture) { - C.glBindTexture(f.glBindTexture, C.GLenum(target), C.GLuint(t.V)) -} - -func (f *Functions) BindVertexArray(a VertexArray) { - C.glBindVertexArray(f.glBindVertexArray, C.GLuint(a.V)) -} - -func (f *Functions) BlendEquation(mode Enum) { - C.glBlendEquation(f.glBlendEquation, C.GLenum(mode)) -} - -func (f *Functions) BlendFuncSeparate(srcRGB, dstRGB, srcA, dstA Enum) { - C.glBlendFuncSeparate(f.glBlendFuncSeparate, C.GLenum(srcRGB), C.GLenum(dstRGB), C.GLenum(srcA), C.GLenum(dstA)) -} - -func (f *Functions) BlitFramebuffer(sx0, sy0, sx1, sy1, dx0, dy0, dx1, dy1 int, mask Enum, filter Enum) { - C.glBlitFramebuffer(f.glBlitFramebuffer, - C.GLint(sx0), C.GLint(sy0), C.GLint(sx1), C.GLint(sy1), - C.GLint(dx0), C.GLint(dy0), C.GLint(dx1), C.GLint(dy1), - C.GLenum(mask), C.GLenum(filter), - ) -} - -func (f *Functions) BufferData(target Enum, size int, usage Enum, data []byte) { - var p unsafe.Pointer - if len(data) > 0 { - p = unsafe.Pointer(&data[0]) - } - C.glBufferData(f.glBufferData, C.GLenum(target), C.GLsizeiptr(size), p, C.GLenum(usage)) -} - -func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { - var p unsafe.Pointer - if len(src) > 0 { - p = unsafe.Pointer(&src[0]) - } - C.glBufferSubData(f.glBufferSubData, C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(len(src)), p) -} - -func (f *Functions) CheckFramebufferStatus(target Enum) Enum { - return Enum(C.glCheckFramebufferStatus(f.glCheckFramebufferStatus, C.GLenum(target))) -} - -func (f *Functions) Clear(mask Enum) { - C.glClear(f.glClear, C.GLbitfield(mask)) -} - -func (f *Functions) ClearColor(red float32, green float32, blue float32, alpha float32) { - C.glClearColor(f.glClearColor, C.GLfloat(red), C.GLfloat(green), C.GLfloat(blue), C.GLfloat(alpha)) -} - -func (f *Functions) ClearDepthf(d float32) { - C.glClearDepthf(f.glClearDepthf, C.GLfloat(d)) -} - -func (f *Functions) CompileShader(s Shader) { - C.glCompileShader(f.glCompileShader, C.GLuint(s.V)) -} - -func (f *Functions) CopyTexSubImage2D(target Enum, level, xoffset, yoffset, x, y, width, height int) { - C.glCopyTexSubImage2D(f.glCopyTexSubImage2D, C.GLenum(target), C.GLint(level), C.GLint(xoffset), C.GLint(yoffset), C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) -} - -func (f *Functions) CreateBuffer() Buffer { - C.glGenBuffers(f.glGenBuffers, 1, &f.uints[0]) - return Buffer{uint(f.uints[0])} -} - -func (f *Functions) CreateFramebuffer() Framebuffer { - C.glGenFramebuffers(f.glGenFramebuffers, 1, &f.uints[0]) - return Framebuffer{uint(f.uints[0])} -} - -func (f *Functions) CreateProgram() Program { - return Program{uint(C.glCreateProgram(f.glCreateProgram))} -} - -func (f *Functions) CreateQuery() Query { - C.glGenQueries(f.glGenQueries, 1, &f.uints[0]) - return Query{uint(f.uints[0])} -} - -func (f *Functions) CreateRenderbuffer() Renderbuffer { - C.glGenRenderbuffers(f.glGenRenderbuffers, 1, &f.uints[0]) - return Renderbuffer{uint(f.uints[0])} -} - -func (f *Functions) CreateShader(ty Enum) Shader { - return Shader{uint(C.glCreateShader(f.glCreateShader, C.GLenum(ty)))} -} - -func (f *Functions) CreateTexture() Texture { - C.glGenTextures(f.glGenTextures, 1, &f.uints[0]) - return Texture{uint(f.uints[0])} -} - -func (f *Functions) CreateVertexArray() VertexArray { - C.glGenVertexArrays(f.glGenVertexArrays, 1, &f.uints[0]) - return VertexArray{uint(f.uints[0])} -} - -func (f *Functions) DeleteBuffer(v Buffer) { - f.uints[0] = C.GLuint(v.V) - C.glDeleteBuffers(f.glDeleteBuffers, 1, &f.uints[0]) -} - -func (f *Functions) DeleteFramebuffer(v Framebuffer) { - f.uints[0] = C.GLuint(v.V) - C.glDeleteFramebuffers(f.glDeleteFramebuffers, 1, &f.uints[0]) -} - -func (f *Functions) DeleteProgram(p Program) { - C.glDeleteProgram(f.glDeleteProgram, C.GLuint(p.V)) -} - -func (f *Functions) DeleteQuery(query Query) { - f.uints[0] = C.GLuint(query.V) - C.glDeleteQueries(f.glDeleteQueries, 1, &f.uints[0]) -} - -func (f *Functions) DeleteVertexArray(array VertexArray) { - f.uints[0] = C.GLuint(array.V) - C.glDeleteVertexArrays(f.glDeleteVertexArrays, 1, &f.uints[0]) -} - -func (f *Functions) DeleteRenderbuffer(v Renderbuffer) { - f.uints[0] = C.GLuint(v.V) - C.glDeleteRenderbuffers(f.glDeleteRenderbuffers, 1, &f.uints[0]) -} - -func (f *Functions) DeleteShader(s Shader) { - C.glDeleteShader(f.glDeleteShader, C.GLuint(s.V)) -} - -func (f *Functions) DeleteTexture(v Texture) { - f.uints[0] = C.GLuint(v.V) - C.glDeleteTextures(f.glDeleteTextures, 1, &f.uints[0]) -} - -func (f *Functions) DepthFunc(v Enum) { - C.glDepthFunc(f.glDepthFunc, C.GLenum(v)) -} - -func (f *Functions) DepthMask(mask bool) { - m := C.GLboolean(FALSE) - if mask { - m = C.GLboolean(TRUE) - } - C.glDepthMask(f.glDepthMask, m) -} - -func (f *Functions) DisableVertexAttribArray(a Attrib) { - C.glDisableVertexAttribArray(f.glDisableVertexAttribArray, C.GLuint(a)) -} - -func (f *Functions) Disable(cap Enum) { - C.glDisable(f.glDisable, C.GLenum(cap)) -} - -func (f *Functions) DrawArrays(mode Enum, first int, count int) { - C.glDrawArrays(f.glDrawArrays, C.GLenum(mode), C.GLint(first), C.GLsizei(count)) -} - -func (f *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { - C.glDrawElements(f.glDrawElements, C.GLenum(mode), C.GLsizei(count), C.GLenum(ty), C.uintptr_t(offset)) -} - -func (f *Functions) DispatchCompute(x, y, z int) { - C.glDispatchCompute(f.glDispatchCompute, C.GLuint(x), C.GLuint(y), C.GLuint(z)) -} - -func (f *Functions) Enable(cap Enum) { - C.glEnable(f.glEnable, C.GLenum(cap)) -} - -func (f *Functions) EndQuery(target Enum) { - C.glEndQuery(f.glEndQuery, C.GLenum(target)) -} - -func (f *Functions) EnableVertexAttribArray(a Attrib) { - C.glEnableVertexAttribArray(f.glEnableVertexAttribArray, C.GLuint(a)) -} - -func (f *Functions) Finish() { - C.glFinish(f.glFinish) -} - -func (f *Functions) Flush() { - C.glFlush(f.glFinish) -} - -func (f *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { - C.glFramebufferRenderbuffer(f.glFramebufferRenderbuffer, C.GLenum(target), C.GLenum(attachment), C.GLenum(renderbuffertarget), C.GLuint(renderbuffer.V)) -} - -func (f *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { - C.glFramebufferTexture2D(f.glFramebufferTexture2D, C.GLenum(target), C.GLenum(attachment), C.GLenum(texTarget), C.GLuint(t.V), C.GLint(level)) -} - -func (f *Functions) GenerateMipmap(target Enum) { - C.glGenerateMipmap(f.glGenerateMipmap, C.GLenum(target)) -} - -func (c *Functions) GetBinding(pname Enum) Object { - return Object{uint(c.GetInteger(pname))} -} - -func (c *Functions) GetBindingi(pname Enum, idx int) Object { - return Object{uint(c.GetIntegeri(pname, idx))} -} - -func (f *Functions) GetError() Enum { - return Enum(C.glGetError(f.glGetError)) -} - -func (f *Functions) GetRenderbufferParameteri(target, pname Enum) int { - C.glGetRenderbufferParameteriv(f.glGetRenderbufferParameteriv, C.GLenum(target), C.GLenum(pname), &f.ints[0]) - return int(f.ints[0]) -} - -func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { - C.glGetFramebufferAttachmentParameteriv(f.glGetFramebufferAttachmentParameteriv, C.GLenum(target), C.GLenum(attachment), C.GLenum(pname), &f.ints[0]) - return int(f.ints[0]) -} - -func (f *Functions) GetFloat4(pname Enum) [4]float32 { - C.glGetFloatv(f.glGetFloatv, C.GLenum(pname), &f.floats[0]) - var r [4]float32 - for i := range r { - r[i] = float32(f.floats[i]) - } - return r -} - -func (f *Functions) GetFloat(pname Enum) float32 { - C.glGetFloatv(f.glGetFloatv, C.GLenum(pname), &f.floats[0]) - return float32(f.floats[0]) -} - -func (f *Functions) GetInteger4(pname Enum) [4]int { - C.glGetIntegerv(f.glGetIntegerv, C.GLenum(pname), &f.ints[0]) - var r [4]int - for i := range r { - r[i] = int(f.ints[i]) - } - return r -} - -func (f *Functions) GetInteger(pname Enum) int { - C.glGetIntegerv(f.glGetIntegerv, C.GLenum(pname), &f.ints[0]) - return int(f.ints[0]) -} - -func (f *Functions) GetIntegeri(pname Enum, idx int) int { - C.glGetIntegeri_v(f.glGetIntegeri_v, C.GLenum(pname), C.GLuint(idx), &f.ints[0]) - return int(f.ints[0]) -} - -func (f *Functions) GetProgrami(p Program, pname Enum) int { - C.glGetProgramiv(f.glGetProgramiv, C.GLuint(p.V), C.GLenum(pname), &f.ints[0]) - return int(f.ints[0]) -} - -func (f *Functions) GetProgramBinary(p Program) []byte { - sz := f.GetProgrami(p, PROGRAM_BINARY_LENGTH) - if sz == 0 { - return nil - } - buf := make([]byte, sz) - var format C.GLenum - C.glGetProgramBinary(f.glGetProgramBinary, C.GLuint(p.V), C.GLsizei(sz), nil, &format, unsafe.Pointer(&buf[0])) - return buf -} - -func (f *Functions) GetProgramInfoLog(p Program) string { - n := f.GetProgrami(p, INFO_LOG_LENGTH) - buf := make([]byte, n) - C.glGetProgramInfoLog(f.glGetProgramInfoLog, C.GLuint(p.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0]))) - return string(buf) -} - -func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { - C.glGetQueryObjectuiv(f.glGetQueryObjectuiv, C.GLuint(query.V), C.GLenum(pname), &f.uints[0]) - return uint(f.uints[0]) -} - -func (f *Functions) GetShaderi(s Shader, pname Enum) int { - C.glGetShaderiv(f.glGetShaderiv, C.GLuint(s.V), C.GLenum(pname), &f.ints[0]) - return int(f.ints[0]) -} - -func (f *Functions) GetShaderInfoLog(s Shader) string { - n := f.GetShaderi(s, INFO_LOG_LENGTH) - buf := make([]byte, n) - C.glGetShaderInfoLog(f.glGetShaderInfoLog, C.GLuint(s.V), C.GLsizei(len(buf)), nil, (*C.GLchar)(unsafe.Pointer(&buf[0]))) - return string(buf) -} - -func (f *Functions) getStringi(pname Enum, index int) string { - str := C.glGetStringi(f.glGetStringi, C.GLenum(pname), C.GLuint(index)) - if str == nil { - return "" - } - return C.GoString((*C.char)(unsafe.Pointer(str))) -} - -func (f *Functions) GetString(pname Enum) string { - switch { - case runtime.GOOS == "darwin" && pname == EXTENSIONS: - // macOS OpenGL 3 core profile doesn't support glGetString(GL_EXTENSIONS). - // Use glGetStringi(GL_EXTENSIONS, ). - var exts []string - nexts := f.GetInteger(NUM_EXTENSIONS) - for i := 0; i < nexts; i++ { - ext := f.getStringi(EXTENSIONS, i) - exts = append(exts, ext) - } - return strings.Join(exts, " ") - default: - str := C.glGetString(f.glGetString, C.GLenum(pname)) - return C.GoString((*C.char)(unsafe.Pointer(str))) - } -} - -func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { - cname := C.CString(name) - defer C.free(unsafe.Pointer(cname)) - return uint(C.glGetUniformBlockIndex(f.glGetUniformBlockIndex, C.GLuint(p.V), cname)) -} - -func (f *Functions) GetUniformLocation(p Program, name string) Uniform { - cname := C.CString(name) - defer C.free(unsafe.Pointer(cname)) - return Uniform{int(C.glGetUniformLocation(f.glGetUniformLocation, C.GLuint(p.V), cname))} -} - -func (f *Functions) GetVertexAttrib(index int, pname Enum) int { - C.glGetVertexAttribiv(f.glGetVertexAttribiv, C.GLuint(index), C.GLenum(pname), &f.ints[0]) - return int(f.ints[0]) -} - -func (f *Functions) GetVertexAttribBinding(index int, pname Enum) Object { - return Object{uint(f.GetVertexAttrib(index, pname))} -} - -func (f *Functions) GetVertexAttribPointer(index int, pname Enum) uintptr { - ptr := C.glGetVertexAttribPointerv(f.glGetVertexAttribPointerv, C.GLuint(index), C.GLenum(pname)) - return uintptr(ptr) -} - -func (f *Functions) InvalidateFramebuffer(target, attachment Enum) { - C.glInvalidateFramebuffer(f.glInvalidateFramebuffer, C.GLenum(target), C.GLenum(attachment)) -} - -func (f *Functions) IsEnabled(cap Enum) bool { - return C.glIsEnabled(f.glIsEnabled, C.GLenum(cap)) == TRUE -} - -func (f *Functions) LinkProgram(p Program) { - C.glLinkProgram(f.glLinkProgram, C.GLuint(p.V)) -} - -func (f *Functions) PixelStorei(pname Enum, param int) { - C.glPixelStorei(f.glPixelStorei, C.GLenum(pname), C.GLint(param)) -} - -func (f *Functions) MemoryBarrier(barriers Enum) { - C.glMemoryBarrier(f.glMemoryBarrier, C.GLbitfield(barriers)) -} - -func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { - p := C.glMapBufferRange(f.glMapBufferRange, C.GLenum(target), C.GLintptr(offset), C.GLsizeiptr(length), C.GLbitfield(access)) - if p == nil { - return nil - } - return (*[1 << 30]byte)(p)[:length:length] -} - -func (f *Functions) Scissor(x, y, width, height int32) { - C.glScissor(f.glScissor, C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) -} - -func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { - var p unsafe.Pointer - if len(data) > 0 { - p = unsafe.Pointer(&data[0]) - } - C.glReadPixels(f.glReadPixels, C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p) -} - -func (f *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { - C.glRenderbufferStorage(f.glRenderbufferStorage, C.GLenum(target), C.GLenum(internalformat), C.GLsizei(width), C.GLsizei(height)) -} - -func (f *Functions) ShaderSource(s Shader, src string) { - csrc := C.CString(src) - defer C.free(unsafe.Pointer(csrc)) - strlen := C.GLint(len(src)) - C.glShaderSource(f.glShaderSource, C.GLuint(s.V), 1, &csrc, &strlen) -} - -func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) { - C.glTexImage2D(f.glTexImage2D, C.GLenum(target), C.GLint(level), C.GLint(internalFormat), C.GLsizei(width), C.GLsizei(height), 0, C.GLenum(format), C.GLenum(ty), nil) -} - -func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { - C.glTexStorage2D(f.glTexStorage2D, C.GLenum(target), C.GLsizei(levels), C.GLenum(internalFormat), C.GLsizei(width), C.GLsizei(height)) -} - -func (f *Functions) TexSubImage2D(target Enum, level int, x int, y int, width int, height int, format Enum, ty Enum, data []byte) { - var p unsafe.Pointer - if len(data) > 0 { - p = unsafe.Pointer(&data[0]) - } - C.glTexSubImage2D(f.glTexSubImage2D, C.GLenum(target), C.GLint(level), C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height), C.GLenum(format), C.GLenum(ty), p) -} - -func (f *Functions) TexParameteri(target, pname Enum, param int) { - C.glTexParameteri(f.glTexParameteri, C.GLenum(target), C.GLenum(pname), C.GLint(param)) -} - -func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { - C.glUniformBlockBinding(f.glUniformBlockBinding, C.GLuint(p.V), C.GLuint(uniformBlockIndex), C.GLuint(uniformBlockBinding)) -} - -func (f *Functions) Uniform1f(dst Uniform, v float32) { - C.glUniform1f(f.glUniform1f, C.GLint(dst.V), C.GLfloat(v)) -} - -func (f *Functions) Uniform1i(dst Uniform, v int) { - C.glUniform1i(f.glUniform1i, C.GLint(dst.V), C.GLint(v)) -} - -func (f *Functions) Uniform2f(dst Uniform, v0 float32, v1 float32) { - C.glUniform2f(f.glUniform2f, C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1)) -} - -func (f *Functions) Uniform3f(dst Uniform, v0 float32, v1 float32, v2 float32) { - C.glUniform3f(f.glUniform3f, C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2)) -} - -func (f *Functions) Uniform4f(dst Uniform, v0 float32, v1 float32, v2 float32, v3 float32) { - C.glUniform4f(f.glUniform4f, C.GLint(dst.V), C.GLfloat(v0), C.GLfloat(v1), C.GLfloat(v2), C.GLfloat(v3)) -} - -func (f *Functions) UseProgram(p Program) { - C.glUseProgram(f.glUseProgram, C.GLuint(p.V)) -} - -func (f *Functions) UnmapBuffer(target Enum) bool { - r := C.glUnmapBuffer(f.glUnmapBuffer, C.GLenum(target)) - return r == TRUE -} - -func (f *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride int, offset int) { - var n C.GLboolean = FALSE - if normalized { - n = TRUE - } - C.glVertexAttribPointer(f.glVertexAttribPointer, C.GLuint(dst), C.GLint(size), C.GLenum(ty), n, C.GLsizei(stride), C.uintptr_t(offset)) -} - -func (f *Functions) Viewport(x int, y int, width int, height int) { - C.glViewport(f.glViewport, C.GLint(x), C.GLint(y), C.GLsizei(width), C.GLsizei(height)) -} diff --git a/gio/internal/gl/gl_windows.go b/gio/internal/gl/gl_windows.go deleted file mode 100644 index 5d6100d..0000000 --- a/gio/internal/gl/gl_windows.go +++ /dev/null @@ -1,721 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gl - -import ( - "fmt" - "math" - "runtime" - "sync" - "syscall" - "unsafe" - - "golang.org/x/sys/windows" -) - -func loadGLESv2Procs() error { - dllName := "libGLESv2.dll" - handle, err := windows.LoadLibraryEx(dllName, 0, windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) - if err != nil { - return fmt.Errorf("gl: failed to load %s: %v", dllName, err) - } - gles := windows.DLL{Handle: handle, Name: dllName} - // d3dcompiler_47.dll is needed internally for shader compilation to function. - dllName = "d3dcompiler_47.dll" - _, err = windows.LoadLibraryEx(dllName, 0, windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) - if err != nil { - return fmt.Errorf("gl: failed to load %s: %v", dllName, err) - } - procs := map[string]**windows.Proc{ - "glActiveTexture": &_glActiveTexture, - "glAttachShader": &_glAttachShader, - "glBeginQuery": &_glBeginQuery, - "glBindAttribLocation": &_glBindAttribLocation, - "glBindBuffer": &_glBindBuffer, - "glBindBufferBase": &_glBindBufferBase, - "glBindFramebuffer": &_glBindFramebuffer, - "glBindRenderbuffer": &_glBindRenderbuffer, - "glBindTexture": &_glBindTexture, - "glBindVertexArray": &_glBindVertexArray, - "glBlendEquation": &_glBlendEquation, - "glBlendFuncSeparate": &_glBlendFuncSeparate, - "glBufferData": &_glBufferData, - "glBufferSubData": &_glBufferSubData, - "glCheckFramebufferStatus": &_glCheckFramebufferStatus, - "glClear": &_glClear, - "glClearColor": &_glClearColor, - "glClearDepthf": &_glClearDepthf, - "glDeleteQueries": &_glDeleteQueries, - "glDeleteVertexArrays": &_glDeleteVertexArrays, - "glCompileShader": &_glCompileShader, - "glCopyTexSubImage2D": &_glCopyTexSubImage2D, - "glGenerateMipmap": &_glGenerateMipmap, - "glGenBuffers": &_glGenBuffers, - "glGenFramebuffers": &_glGenFramebuffers, - "glGenVertexArrays": &_glGenVertexArrays, - "glGetUniformBlockIndex": &_glGetUniformBlockIndex, - "glCreateProgram": &_glCreateProgram, - "glGenRenderbuffers": &_glGenRenderbuffers, - "glCreateShader": &_glCreateShader, - "glGenTextures": &_glGenTextures, - "glDeleteBuffers": &_glDeleteBuffers, - "glDeleteFramebuffers": &_glDeleteFramebuffers, - "glDeleteProgram": &_glDeleteProgram, - "glDeleteShader": &_glDeleteShader, - "glDeleteRenderbuffers": &_glDeleteRenderbuffers, - "glDeleteTextures": &_glDeleteTextures, - "glDepthFunc": &_glDepthFunc, - "glDepthMask": &_glDepthMask, - "glDisableVertexAttribArray": &_glDisableVertexAttribArray, - "glDisable": &_glDisable, - "glDrawArrays": &_glDrawArrays, - "glDrawElements": &_glDrawElements, - "glEnable": &_glEnable, - "glEnableVertexAttribArray": &_glEnableVertexAttribArray, - "glEndQuery": &_glEndQuery, - "glFinish": &_glFinish, - "glFlush": &_glFlush, - "glFramebufferRenderbuffer": &_glFramebufferRenderbuffer, - "glFramebufferTexture2D": &_glFramebufferTexture2D, - "glGenQueries": &_glGenQueries, - "glGetError": &_glGetError, - "glGetRenderbufferParameteriv": &_glGetRenderbufferParameteriv, - "glGetFloatv": &_glGetFloatv, - "glGetFramebufferAttachmentParameteriv": &_glGetFramebufferAttachmentParameteriv, - "glGetIntegerv": &_glGetIntegerv, - "glGetIntegeri_v": &_glGetIntegeri_v, - "glGetProgramiv": &_glGetProgramiv, - "glGetProgramInfoLog": &_glGetProgramInfoLog, - "glGetQueryObjectuiv": &_glGetQueryObjectuiv, - "glGetShaderiv": &_glGetShaderiv, - "glGetShaderInfoLog": &_glGetShaderInfoLog, - "glGetString": &_glGetString, - "glGetUniformLocation": &_glGetUniformLocation, - "glGetVertexAttribiv": &_glGetVertexAttribiv, - "glGetVertexAttribPointerv": &_glGetVertexAttribPointerv, - "glInvalidateFramebuffer": &_glInvalidateFramebuffer, - "glIsEnabled": &_glIsEnabled, - "glLinkProgram": &_glLinkProgram, - "glPixelStorei": &_glPixelStorei, - "glReadPixels": &_glReadPixels, - "glRenderbufferStorage": &_glRenderbufferStorage, - "glScissor": &_glScissor, - "glShaderSource": &_glShaderSource, - "glTexImage2D": &_glTexImage2D, - "glTexStorage2D": &_glTexStorage2D, - "glTexSubImage2D": &_glTexSubImage2D, - "glTexParameteri": &_glTexParameteri, - "glUniformBlockBinding": &_glUniformBlockBinding, - "glUniform1f": &_glUniform1f, - "glUniform1i": &_glUniform1i, - "glUniform2f": &_glUniform2f, - "glUniform3f": &_glUniform3f, - "glUniform4f": &_glUniform4f, - "glUseProgram": &_glUseProgram, - "glVertexAttribPointer": &_glVertexAttribPointer, - "glViewport": &_glViewport, - } - for name, proc := range procs { - p, err := gles.FindProc(name) - if err != nil { - return fmt.Errorf("failed to locate %s in %s: %w", name, gles.Name, err) - } - *proc = p - } - return nil -} - -var ( - glInitOnce sync.Once - _glActiveTexture *windows.Proc - _glAttachShader *windows.Proc - _glBeginQuery *windows.Proc - _glBindAttribLocation *windows.Proc - _glBindBuffer *windows.Proc - _glBindBufferBase *windows.Proc - _glBindFramebuffer *windows.Proc - _glBindRenderbuffer *windows.Proc - _glBindTexture *windows.Proc - _glBindVertexArray *windows.Proc - _glBlendEquation *windows.Proc - _glBlendFuncSeparate *windows.Proc - _glBufferData *windows.Proc - _glBufferSubData *windows.Proc - _glCheckFramebufferStatus *windows.Proc - _glClear *windows.Proc - _glClearColor *windows.Proc - _glClearDepthf *windows.Proc - _glDeleteQueries *windows.Proc - _glDeleteVertexArrays *windows.Proc - _glCompileShader *windows.Proc - _glCopyTexSubImage2D *windows.Proc - _glGenerateMipmap *windows.Proc - _glGenBuffers *windows.Proc - _glGenFramebuffers *windows.Proc - _glGenVertexArrays *windows.Proc - _glGetUniformBlockIndex *windows.Proc - _glCreateProgram *windows.Proc - _glGenRenderbuffers *windows.Proc - _glCreateShader *windows.Proc - _glGenTextures *windows.Proc - _glDeleteBuffers *windows.Proc - _glDeleteFramebuffers *windows.Proc - _glDeleteProgram *windows.Proc - _glDeleteShader *windows.Proc - _glDeleteRenderbuffers *windows.Proc - _glDeleteTextures *windows.Proc - _glDepthFunc *windows.Proc - _glDepthMask *windows.Proc - _glDisableVertexAttribArray *windows.Proc - _glDisable *windows.Proc - _glDrawArrays *windows.Proc - _glDrawElements *windows.Proc - _glEnable *windows.Proc - _glEnableVertexAttribArray *windows.Proc - _glEndQuery *windows.Proc - _glFinish *windows.Proc - _glFlush *windows.Proc - _glFramebufferRenderbuffer *windows.Proc - _glFramebufferTexture2D *windows.Proc - _glGenQueries *windows.Proc - _glGetError *windows.Proc - _glGetRenderbufferParameteriv *windows.Proc - _glGetFloatv *windows.Proc - _glGetFramebufferAttachmentParameteriv *windows.Proc - _glGetIntegerv *windows.Proc - _glGetIntegeri_v *windows.Proc - _glGetProgramiv *windows.Proc - _glGetProgramInfoLog *windows.Proc - _glGetQueryObjectuiv *windows.Proc - _glGetShaderiv *windows.Proc - _glGetShaderInfoLog *windows.Proc - _glGetString *windows.Proc - _glGetUniformLocation *windows.Proc - _glGetVertexAttribiv *windows.Proc - _glGetVertexAttribPointerv *windows.Proc - _glInvalidateFramebuffer *windows.Proc - _glIsEnabled *windows.Proc - _glLinkProgram *windows.Proc - _glPixelStorei *windows.Proc - _glReadPixels *windows.Proc - _glRenderbufferStorage *windows.Proc - _glScissor *windows.Proc - _glShaderSource *windows.Proc - _glTexImage2D *windows.Proc - _glTexStorage2D *windows.Proc - _glTexSubImage2D *windows.Proc - _glTexParameteri *windows.Proc - _glUniformBlockBinding *windows.Proc - _glUniform1f *windows.Proc - _glUniform1i *windows.Proc - _glUniform2f *windows.Proc - _glUniform3f *windows.Proc - _glUniform4f *windows.Proc - _glUseProgram *windows.Proc - _glVertexAttribPointer *windows.Proc - _glViewport *windows.Proc -) - -type Functions struct { - // Query caches. - int32s [100]int32 - float32s [100]float32 - uintptrs [100]uintptr -} - -type Context any - -func NewFunctions(ctx Context, forceES bool) (*Functions, error) { - if ctx != nil { - panic("non-nil context") - } - var err error - glInitOnce.Do(func() { - err = loadGLESv2Procs() - }) - return new(Functions), err -} - -func (c *Functions) ActiveTexture(t Enum) { - syscall.Syscall(_glActiveTexture.Addr(), 1, uintptr(t), 0, 0) -} - -func (c *Functions) AttachShader(p Program, s Shader) { - syscall.Syscall(_glAttachShader.Addr(), 2, uintptr(p.V), uintptr(s.V), 0) -} - -func (f *Functions) BeginQuery(target Enum, query Query) { - syscall.Syscall(_glBeginQuery.Addr(), 2, uintptr(target), uintptr(query.V), 0) -} - -func (c *Functions) BindAttribLocation(p Program, a Attrib, name string) { - cname := cString(name) - c0 := &cname[0] - syscall.Syscall(_glBindAttribLocation.Addr(), 3, uintptr(p.V), uintptr(a), uintptr(unsafe.Pointer(c0))) - issue34474KeepAlive(c) -} - -func (c *Functions) BindBuffer(target Enum, b Buffer) { - syscall.Syscall(_glBindBuffer.Addr(), 2, uintptr(target), uintptr(b.V), 0) -} - -func (c *Functions) BindBufferBase(target Enum, index int, b Buffer) { - syscall.Syscall(_glBindBufferBase.Addr(), 3, uintptr(target), uintptr(index), uintptr(b.V)) -} - -func (c *Functions) BindFramebuffer(target Enum, fb Framebuffer) { - syscall.Syscall(_glBindFramebuffer.Addr(), 2, uintptr(target), uintptr(fb.V), 0) -} - -func (c *Functions) BindRenderbuffer(target Enum, rb Renderbuffer) { - syscall.Syscall(_glBindRenderbuffer.Addr(), 2, uintptr(target), uintptr(rb.V), 0) -} - -func (f *Functions) BindImageTexture(unit int, t Texture, level int, layered bool, layer int, access, format Enum) { - panic("not implemented") -} - -func (c *Functions) BindTexture(target Enum, t Texture) { - syscall.Syscall(_glBindTexture.Addr(), 2, uintptr(target), uintptr(t.V), 0) -} - -func (c *Functions) BindVertexArray(a VertexArray) { - syscall.Syscall(_glBindVertexArray.Addr(), 1, uintptr(a.V), 0, 0) -} - -func (c *Functions) BlendEquation(mode Enum) { - syscall.Syscall(_glBlendEquation.Addr(), 1, uintptr(mode), 0, 0) -} - -func (c *Functions) BlendFuncSeparate(srcRGB, dstRGB, srcA, dstA Enum) { - syscall.Syscall6(_glBlendFuncSeparate.Addr(), 4, uintptr(srcRGB), uintptr(dstRGB), uintptr(srcA), uintptr(dstA), 0, 0) -} - -func (c *Functions) BufferData(target Enum, size int, usage Enum, data []byte) { - var p unsafe.Pointer - if len(data) > 0 { - p = unsafe.Pointer(&data[0]) - } - syscall.Syscall6(_glBufferData.Addr(), 4, uintptr(target), uintptr(size), uintptr(p), uintptr(usage), 0, 0) -} - -func (f *Functions) BufferSubData(target Enum, offset int, src []byte) { - if n := len(src); n > 0 { - s0 := &src[0] - syscall.Syscall6(_glBufferSubData.Addr(), 4, uintptr(target), uintptr(offset), uintptr(n), uintptr(unsafe.Pointer(s0)), 0, 0) - issue34474KeepAlive(s0) - } -} - -func (c *Functions) CheckFramebufferStatus(target Enum) Enum { - s, _, _ := syscall.Syscall(_glCheckFramebufferStatus.Addr(), 1, uintptr(target), 0, 0) - return Enum(s) -} - -func (c *Functions) Clear(mask Enum) { - syscall.Syscall(_glClear.Addr(), 1, uintptr(mask), 0, 0) -} - -func (c *Functions) ClearColor(red, green, blue, alpha float32) { - syscall.Syscall6(_glClearColor.Addr(), 4, uintptr(math.Float32bits(red)), uintptr(math.Float32bits(green)), uintptr(math.Float32bits(blue)), uintptr(math.Float32bits(alpha)), 0, 0) -} - -func (c *Functions) ClearDepthf(d float32) { - syscall.Syscall(_glClearDepthf.Addr(), 1, uintptr(math.Float32bits(d)), 0, 0) -} - -func (c *Functions) CompileShader(s Shader) { - syscall.Syscall(_glCompileShader.Addr(), 1, uintptr(s.V), 0, 0) -} - -func (f *Functions) CopyTexSubImage2D(target Enum, level, xoffset, yoffset, x, y, width, height int) { - syscall.Syscall9(_glCopyTexSubImage2D.Addr(), 8, uintptr(target), uintptr(level), uintptr(xoffset), uintptr(yoffset), uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0) -} - -func (f *Functions) GenerateMipmap(target Enum) { - syscall.Syscall(_glGenerateMipmap.Addr(), 1, uintptr(target), 0, 0) -} - -func (c *Functions) CreateBuffer() Buffer { - var buf uintptr - syscall.Syscall(_glGenBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&buf)), 0) - return Buffer{uint(buf)} -} - -func (c *Functions) CreateFramebuffer() Framebuffer { - var fb uintptr - syscall.Syscall(_glGenFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&fb)), 0) - return Framebuffer{uint(fb)} -} - -func (c *Functions) CreateProgram() Program { - p, _, _ := syscall.Syscall(_glCreateProgram.Addr(), 0, 0, 0, 0) - return Program{uint(p)} -} - -func (f *Functions) CreateQuery() Query { - var q uintptr - syscall.Syscall(_glGenQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&q)), 0) - return Query{uint(q)} -} - -func (c *Functions) CreateRenderbuffer() Renderbuffer { - var rb uintptr - syscall.Syscall(_glGenRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&rb)), 0) - return Renderbuffer{uint(rb)} -} - -func (c *Functions) CreateShader(ty Enum) Shader { - s, _, _ := syscall.Syscall(_glCreateShader.Addr(), 1, uintptr(ty), 0, 0) - return Shader{uint(s)} -} - -func (c *Functions) CreateTexture() Texture { - var t uintptr - syscall.Syscall(_glGenTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&t)), 0) - return Texture{uint(t)} -} - -func (c *Functions) CreateVertexArray() VertexArray { - var t uintptr - syscall.Syscall(_glGenVertexArrays.Addr(), 2, 1, uintptr(unsafe.Pointer(&t)), 0) - return VertexArray{uint(t)} -} - -func (c *Functions) DeleteBuffer(v Buffer) { - syscall.Syscall(_glDeleteBuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v)), 0) -} - -func (c *Functions) DeleteFramebuffer(v Framebuffer) { - syscall.Syscall(_glDeleteFramebuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) -} - -func (c *Functions) DeleteProgram(p Program) { - syscall.Syscall(_glDeleteProgram.Addr(), 1, uintptr(p.V), 0, 0) -} - -func (f *Functions) DeleteQuery(query Query) { - syscall.Syscall(_glDeleteQueries.Addr(), 2, 1, uintptr(unsafe.Pointer(&query.V)), 0) -} - -func (c *Functions) DeleteShader(s Shader) { - syscall.Syscall(_glDeleteShader.Addr(), 1, uintptr(s.V), 0, 0) -} - -func (c *Functions) DeleteRenderbuffer(v Renderbuffer) { - syscall.Syscall(_glDeleteRenderbuffers.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) -} - -func (c *Functions) DeleteTexture(v Texture) { - syscall.Syscall(_glDeleteTextures.Addr(), 2, 1, uintptr(unsafe.Pointer(&v.V)), 0) -} - -func (f *Functions) DeleteVertexArray(array VertexArray) { - syscall.Syscall(_glDeleteVertexArrays.Addr(), 2, 1, uintptr(unsafe.Pointer(&array.V)), 0) -} - -func (c *Functions) DepthFunc(f Enum) { - syscall.Syscall(_glDepthFunc.Addr(), 1, uintptr(f), 0, 0) -} - -func (c *Functions) DepthMask(mask bool) { - var m uintptr - if mask { - m = 1 - } - syscall.Syscall(_glDepthMask.Addr(), 1, m, 0, 0) -} - -func (c *Functions) DisableVertexAttribArray(a Attrib) { - syscall.Syscall(_glDisableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) -} - -func (c *Functions) Disable(cap Enum) { - syscall.Syscall(_glDisable.Addr(), 1, uintptr(cap), 0, 0) -} - -func (c *Functions) DrawArrays(mode Enum, first, count int) { - syscall.Syscall(_glDrawArrays.Addr(), 3, uintptr(mode), uintptr(first), uintptr(count)) -} - -func (c *Functions) DrawElements(mode Enum, count int, ty Enum, offset int) { - syscall.Syscall6(_glDrawElements.Addr(), 4, uintptr(mode), uintptr(count), uintptr(ty), uintptr(offset), 0, 0) -} - -func (f *Functions) DispatchCompute(x, y, z int) { - panic("not implemented") -} - -func (c *Functions) Enable(cap Enum) { - syscall.Syscall(_glEnable.Addr(), 1, uintptr(cap), 0, 0) -} - -func (c *Functions) EnableVertexAttribArray(a Attrib) { - syscall.Syscall(_glEnableVertexAttribArray.Addr(), 1, uintptr(a), 0, 0) -} - -func (f *Functions) EndQuery(target Enum) { - syscall.Syscall(_glEndQuery.Addr(), 1, uintptr(target), 0, 0) -} - -func (c *Functions) Finish() { - syscall.Syscall(_glFinish.Addr(), 0, 0, 0, 0) -} - -func (c *Functions) Flush() { - syscall.Syscall(_glFlush.Addr(), 0, 0, 0, 0) -} - -func (c *Functions) FramebufferRenderbuffer(target, attachment, renderbuffertarget Enum, renderbuffer Renderbuffer) { - syscall.Syscall6(_glFramebufferRenderbuffer.Addr(), 4, uintptr(target), uintptr(attachment), uintptr(renderbuffertarget), uintptr(renderbuffer.V), 0, 0) -} - -func (c *Functions) FramebufferTexture2D(target, attachment, texTarget Enum, t Texture, level int) { - syscall.Syscall6(_glFramebufferTexture2D.Addr(), 5, uintptr(target), uintptr(attachment), uintptr(texTarget), uintptr(t.V), uintptr(level), 0) -} - -func (f *Functions) GetUniformBlockIndex(p Program, name string) uint { - cname := cString(name) - c0 := &cname[0] - u, _, _ := syscall.Syscall(_glGetUniformBlockIndex.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0) - issue34474KeepAlive(c0) - return uint(u) -} - -func (c *Functions) GetBinding(pname Enum) Object { - return Object{uint(c.GetInteger(pname))} -} - -func (c *Functions) GetBindingi(pname Enum, idx int) Object { - return Object{uint(c.GetIntegeri(pname, idx))} -} - -func (c *Functions) GetError() Enum { - e, _, _ := syscall.Syscall(_glGetError.Addr(), 0, 0, 0, 0) - return Enum(e) -} - -func (c *Functions) GetRenderbufferParameteri(target, pname Enum) int { - syscall.Syscall(_glGetRenderbufferParameteriv.Addr(), 3, uintptr(target), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) - return int(c.int32s[0]) -} - -func (c *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname Enum) int { - syscall.Syscall6(_glGetFramebufferAttachmentParameteriv.Addr(), 4, uintptr(target), uintptr(attachment), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0, 0) - return int(c.int32s[0]) -} - -func (c *Functions) GetInteger4(pname Enum) [4]int { - syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0) - var r [4]int - for i := range r { - r[i] = int(c.int32s[i]) - } - return r -} - -func (c *Functions) GetInteger(pname Enum) int { - syscall.Syscall(_glGetIntegerv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0])), 0) - return int(c.int32s[0]) -} - -func (c *Functions) GetIntegeri(pname Enum, idx int) int { - syscall.Syscall(_glGetIntegeri_v.Addr(), 3, uintptr(pname), uintptr(idx), uintptr(unsafe.Pointer(&c.int32s[0]))) - return int(c.int32s[0]) -} - -func (c *Functions) GetFloat(pname Enum) float32 { - syscall.Syscall(_glGetFloatv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.float32s[0])), 0) - return c.float32s[0] -} - -func (c *Functions) GetFloat4(pname Enum) [4]float32 { - syscall.Syscall(_glGetFloatv.Addr(), 2, uintptr(pname), uintptr(unsafe.Pointer(&c.float32s[0])), 0) - var r [4]float32 - copy(r[:], c.float32s[:]) - return r -} - -func (c *Functions) GetProgrami(p Program, pname Enum) int { - syscall.Syscall(_glGetProgramiv.Addr(), 3, uintptr(p.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) - return int(c.int32s[0]) -} - -func (c *Functions) GetProgramInfoLog(p Program) string { - n := c.GetProgrami(p, INFO_LOG_LENGTH) - if n == 0 { - return "" - } - buf := make([]byte, n) - syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) - return string(buf) -} - -func (c *Functions) GetQueryObjectuiv(query Query, pname Enum) uint { - syscall.Syscall(_glGetQueryObjectuiv.Addr(), 3, uintptr(query.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) - return uint(c.int32s[0]) -} - -func (c *Functions) GetShaderi(s Shader, pname Enum) int { - syscall.Syscall(_glGetShaderiv.Addr(), 3, uintptr(s.V), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) - return int(c.int32s[0]) -} - -func (c *Functions) GetShaderInfoLog(s Shader) string { - n := c.GetShaderi(s, INFO_LOG_LENGTH) - buf := make([]byte, n) - syscall.Syscall6(_glGetShaderInfoLog.Addr(), 4, uintptr(s.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) - return string(buf) -} - -func (c *Functions) GetString(pname Enum) string { - s, _, _ := syscall.Syscall(_glGetString.Addr(), 1, uintptr(pname), 0, 0) - return windows.BytePtrToString((*byte)(unsafe.Pointer(s))) -} - -func (c *Functions) GetUniformLocation(p Program, name string) Uniform { - cname := cString(name) - c0 := &cname[0] - u, _, _ := syscall.Syscall(_glGetUniformLocation.Addr(), 2, uintptr(p.V), uintptr(unsafe.Pointer(c0)), 0) - issue34474KeepAlive(c0) - return Uniform{int(u)} -} - -func (c *Functions) GetVertexAttrib(index int, pname Enum) int { - syscall.Syscall(_glGetVertexAttribiv.Addr(), 3, uintptr(index), uintptr(pname), uintptr(unsafe.Pointer(&c.int32s[0]))) - return int(c.int32s[0]) -} - -func (c *Functions) GetVertexAttribBinding(index int, pname Enum) Object { - return Object{uint(c.GetVertexAttrib(index, pname))} -} - -func (c *Functions) GetVertexAttribPointer(index int, pname Enum) uintptr { - syscall.Syscall(_glGetVertexAttribPointerv.Addr(), 3, uintptr(index), uintptr(pname), uintptr(unsafe.Pointer(&c.uintptrs[0]))) - return c.uintptrs[0] -} - -func (c *Functions) InvalidateFramebuffer(target, attachment Enum) { - addr := _glInvalidateFramebuffer.Addr() - if addr == 0 { - // InvalidateFramebuffer is just a hint. Skip it if not supported. - return - } - syscall.Syscall(addr, 3, uintptr(target), 1, uintptr(unsafe.Pointer(&attachment))) -} - -func (f *Functions) IsEnabled(cap Enum) bool { - u, _, _ := syscall.Syscall(_glIsEnabled.Addr(), 1, uintptr(cap), 0, 0) - return u == TRUE -} - -func (c *Functions) LinkProgram(p Program) { - syscall.Syscall(_glLinkProgram.Addr(), 1, uintptr(p.V), 0, 0) -} - -func (c *Functions) PixelStorei(pname Enum, param int) { - syscall.Syscall(_glPixelStorei.Addr(), 2, uintptr(pname), uintptr(param), 0) -} - -func (f *Functions) MemoryBarrier(barriers Enum) { - panic("not implemented") -} - -func (f *Functions) MapBufferRange(target Enum, offset, length int, access Enum) []byte { - panic("not implemented") -} - -func (f *Functions) ReadPixels(x, y, width, height int, format, ty Enum, data []byte) { - d0 := &data[0] - syscall.Syscall9(_glReadPixels.Addr(), 7, uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0)), 0, 0) - issue34474KeepAlive(d0) -} - -func (c *Functions) RenderbufferStorage(target, internalformat Enum, width, height int) { - syscall.Syscall6(_glRenderbufferStorage.Addr(), 4, uintptr(target), uintptr(internalformat), uintptr(width), uintptr(height), 0, 0) -} - -func (c *Functions) Scissor(x, y, width, height int32) { - syscall.Syscall6(_glScissor.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) -} - -func (c *Functions) ShaderSource(s Shader, src string) { - var n uintptr = uintptr(len(src)) - psrc := &src - syscall.Syscall6(_glShaderSource.Addr(), 4, uintptr(s.V), 1, uintptr(unsafe.Pointer(psrc)), uintptr(unsafe.Pointer(&n)), 0, 0) - issue34474KeepAlive(psrc) -} - -func (f *Functions) TexImage2D(target Enum, level int, internalFormat Enum, width int, height int, format Enum, ty Enum) { - syscall.Syscall9(_glTexImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(internalFormat), uintptr(width), uintptr(height), 0, uintptr(format), uintptr(ty), 0) -} - -func (f *Functions) TexStorage2D(target Enum, levels int, internalFormat Enum, width, height int) { - syscall.Syscall6(_glTexStorage2D.Addr(), 5, uintptr(target), uintptr(levels), uintptr(internalFormat), uintptr(width), uintptr(height), 0) -} - -func (c *Functions) TexSubImage2D(target Enum, level int, x, y, width, height int, format, ty Enum, data []byte) { - d0 := &data[0] - syscall.Syscall9(_glTexSubImage2D.Addr(), 9, uintptr(target), uintptr(level), uintptr(x), uintptr(y), uintptr(width), uintptr(height), uintptr(format), uintptr(ty), uintptr(unsafe.Pointer(d0))) - issue34474KeepAlive(d0) -} - -func (c *Functions) TexParameteri(target, pname Enum, param int) { - syscall.Syscall(_glTexParameteri.Addr(), 3, uintptr(target), uintptr(pname), uintptr(param)) -} - -func (f *Functions) UniformBlockBinding(p Program, uniformBlockIndex uint, uniformBlockBinding uint) { - syscall.Syscall(_glUniformBlockBinding.Addr(), 3, uintptr(p.V), uintptr(uniformBlockIndex), uintptr(uniformBlockBinding)) -} - -func (c *Functions) Uniform1f(dst Uniform, v float32) { - syscall.Syscall(_glUniform1f.Addr(), 2, uintptr(dst.V), uintptr(math.Float32bits(v)), 0) -} - -func (c *Functions) Uniform1i(dst Uniform, v int) { - syscall.Syscall(_glUniform1i.Addr(), 2, uintptr(dst.V), uintptr(v), 0) -} - -func (c *Functions) Uniform2f(dst Uniform, v0, v1 float32) { - syscall.Syscall(_glUniform2f.Addr(), 3, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1))) -} - -func (c *Functions) Uniform3f(dst Uniform, v0, v1, v2 float32) { - syscall.Syscall6(_glUniform3f.Addr(), 4, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), 0, 0) -} - -func (c *Functions) Uniform4f(dst Uniform, v0, v1, v2, v3 float32) { - syscall.Syscall6(_glUniform4f.Addr(), 5, uintptr(dst.V), uintptr(math.Float32bits(v0)), uintptr(math.Float32bits(v1)), uintptr(math.Float32bits(v2)), uintptr(math.Float32bits(v3)), 0) -} - -func (c *Functions) UseProgram(p Program) { - syscall.Syscall(_glUseProgram.Addr(), 1, uintptr(p.V), 0, 0) -} - -func (f *Functions) UnmapBuffer(target Enum) bool { - panic("not implemented") -} - -func (c *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalized bool, stride, offset int) { - var norm uintptr - if normalized { - norm = 1 - } - syscall.Syscall6(_glVertexAttribPointer.Addr(), 6, uintptr(dst), uintptr(size), uintptr(ty), norm, uintptr(stride), uintptr(offset)) -} - -func (c *Functions) Viewport(x, y, width, height int) { - syscall.Syscall6(_glViewport.Addr(), 4, uintptr(x), uintptr(y), uintptr(width), uintptr(height), 0, 0) -} - -func cString(s string) []byte { - b := make([]byte, len(s)+1) - copy(b, s) - return b -} - -// issue34474KeepAlive calls runtime.KeepAlive as a -// workaround for golang.org/issue/34474. -func issue34474KeepAlive(v any) { - runtime.KeepAlive(v) -} diff --git a/gio/internal/gl/types.go b/gio/internal/gl/types.go deleted file mode 100644 index dd24963..0000000 --- a/gio/internal/gl/types.go +++ /dev/null @@ -1,77 +0,0 @@ -//go:build !js -// +build !js - -package gl - -type ( - Object struct{ V uint } - Buffer Object - Framebuffer Object - Program Object - Renderbuffer Object - Shader Object - Texture Object - Query Object - Uniform struct{ V int } - VertexArray Object -) - -func (o Object) valid() bool { - return o.V != 0 -} - -func (o Object) equal(o2 Object) bool { - return o == o2 -} - -func (u Framebuffer) Valid() bool { - return Object(u).valid() -} - -func (u Uniform) Valid() bool { - return u.V != -1 -} - -func (p Program) Valid() bool { - return Object(p).valid() -} - -func (s Shader) Valid() bool { - return Object(s).valid() -} - -func (a VertexArray) Valid() bool { - return Object(a).valid() -} - -func (f Framebuffer) Equal(f2 Framebuffer) bool { - return Object(f).equal(Object(f2)) -} - -func (p Program) Equal(p2 Program) bool { - return Object(p).equal(Object(p2)) -} - -func (s Shader) Equal(s2 Shader) bool { - return Object(s).equal(Object(s2)) -} - -func (u Uniform) Equal(u2 Uniform) bool { - return u == u2 -} - -func (a VertexArray) Equal(a2 VertexArray) bool { - return Object(a).equal(Object(a2)) -} - -func (r Renderbuffer) Equal(r2 Renderbuffer) bool { - return Object(r).equal(Object(r2)) -} - -func (t Texture) Equal(t2 Texture) bool { - return Object(t).equal(Object(t2)) -} - -func (b Buffer) Equal(b2 Buffer) bool { - return Object(b).equal(Object(b2)) -} diff --git a/gio/internal/gl/types_js.go b/gio/internal/gl/types_js.go deleted file mode 100644 index 8d91a6b..0000000 --- a/gio/internal/gl/types_js.go +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gl - -import "syscall/js" - -type ( - Object js.Value - Buffer Object - Framebuffer Object - Program Object - Renderbuffer Object - Shader Object - Texture Object - Query Object - Uniform Object - VertexArray Object -) - -func (o Object) valid() bool { - return js.Value(o).Truthy() -} - -func (o Object) equal(o2 Object) bool { - return js.Value(o).Equal(js.Value(o2)) -} - -func (b Buffer) Valid() bool { - return Object(b).valid() -} - -func (f Framebuffer) Valid() bool { - return Object(f).valid() -} - -func (p Program) Valid() bool { - return Object(p).valid() -} - -func (r Renderbuffer) Valid() bool { - return Object(r).valid() -} - -func (s Shader) Valid() bool { - return Object(s).valid() -} - -func (t Texture) Valid() bool { - return Object(t).valid() -} - -func (u Uniform) Valid() bool { - return Object(u).valid() -} - -func (a VertexArray) Valid() bool { - return Object(a).valid() -} - -func (f Framebuffer) Equal(f2 Framebuffer) bool { - return Object(f).equal(Object(f2)) -} - -func (p Program) Equal(p2 Program) bool { - return Object(p).equal(Object(p2)) -} - -func (s Shader) Equal(s2 Shader) bool { - return Object(s).equal(Object(s2)) -} - -func (u Uniform) Equal(u2 Uniform) bool { - return Object(u).equal(Object(u2)) -} - -func (a VertexArray) Equal(a2 VertexArray) bool { - return Object(a).equal(Object(a2)) -} - -func (r Renderbuffer) Equal(r2 Renderbuffer) bool { - return Object(r).equal(Object(r2)) -} - -func (t Texture) Equal(t2 Texture) bool { - return Object(t).equal(Object(t2)) -} - -func (b Buffer) Equal(b2 Buffer) bool { - return Object(b).equal(Object(b2)) -} diff --git a/gio/internal/gl/util.go b/gio/internal/gl/util.go deleted file mode 100644 index c696b69..0000000 --- a/gio/internal/gl/util.go +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gl - -import ( - "errors" - "fmt" - "strings" -) - -func CreateProgram(ctx *Functions, vsSrc, fsSrc string, attribs []string) (Program, error) { - vs, err := CreateShader(ctx, VERTEX_SHADER, vsSrc) - if err != nil { - return Program{}, err - } - defer ctx.DeleteShader(vs) - fs, err := CreateShader(ctx, FRAGMENT_SHADER, fsSrc) - if err != nil { - return Program{}, err - } - defer ctx.DeleteShader(fs) - prog := ctx.CreateProgram() - if !prog.Valid() { - return Program{}, errors.New("glCreateProgram failed") - } - ctx.AttachShader(prog, vs) - ctx.AttachShader(prog, fs) - for i, a := range attribs { - ctx.BindAttribLocation(prog, Attrib(i), a) - } - ctx.LinkProgram(prog) - if ctx.GetProgrami(prog, LINK_STATUS) == 0 { - log := ctx.GetProgramInfoLog(prog) - ctx.DeleteProgram(prog) - return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) - } - return prog, nil -} - -func CreateComputeProgram(ctx *Functions, src string) (Program, error) { - cs, err := CreateShader(ctx, COMPUTE_SHADER, src) - if err != nil { - return Program{}, err - } - defer ctx.DeleteShader(cs) - prog := ctx.CreateProgram() - if !prog.Valid() { - return Program{}, errors.New("glCreateProgram failed") - } - ctx.AttachShader(prog, cs) - ctx.LinkProgram(prog) - if ctx.GetProgrami(prog, LINK_STATUS) == 0 { - log := ctx.GetProgramInfoLog(prog) - ctx.DeleteProgram(prog) - return Program{}, fmt.Errorf("program link failed: %s", strings.TrimSpace(log)) - } - return prog, nil -} - -func CreateShader(ctx *Functions, typ Enum, src string) (Shader, error) { - sh := ctx.CreateShader(typ) - if !sh.Valid() { - return Shader{}, errors.New("glCreateShader failed") - } - ctx.ShaderSource(sh, src) - ctx.CompileShader(sh) - if ctx.GetShaderi(sh, COMPILE_STATUS) == 0 { - log := ctx.GetShaderInfoLog(sh) - ctx.DeleteShader(sh) - return Shader{}, fmt.Errorf("shader compilation failed: %s", strings.TrimSpace(log)) - } - return sh, nil -} - -func ParseGLVersion(glVer string) (version [2]int, gles bool, err error) { - var ver [2]int - if _, err := fmt.Sscanf(glVer, "OpenGL ES %d.%d", &ver[0], &ver[1]); err == nil { - return ver, true, nil - } else if _, err := fmt.Sscanf(glVer, "WebGL %d.%d", &ver[0], &ver[1]); err == nil { - // WebGL major version v corresponds to OpenGL ES version v + 1 - ver[0]++ - return ver, true, nil - } else if _, err := fmt.Sscanf(glVer, "%d.%d", &ver[0], &ver[1]); err == nil { - return ver, false, nil - } - return ver, false, fmt.Errorf("failed to parse OpenGL ES version (%s)", glVer) -} diff --git a/gio/internal/ops/ops.go b/gio/internal/ops/ops.go deleted file mode 100644 index 46efd95..0000000 --- a/gio/internal/ops/ops.go +++ /dev/null @@ -1,497 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package ops - -import ( - "encoding/binary" - "image" - "math" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/byteslice" - "github.com/p9c/p9/pkg/gel/gio/internal/scene" -) - -type Ops struct { - // version is incremented at each Reset. - version uint32 - // data contains the serialized operations. - data []byte - // refs hold external references for operations. - refs []any - // stringRefs provides space for string references, pointers to which will - // be stored in refs. Storing a string directly in refs would cause a heap - // allocation, to store the string header in an interface value. The backing - // array of stringRefs, on the other hand, gets reused between calls to - // reset, making string references free on average. - // - // Appending to stringRefs might reallocate the backing array, which will - // leave pointers to the old array in refs. This temporarily causes a slight - // increase in memory usage, but this, too, amortizes away as the capacity - // of stringRefs approaches its stable maximum. - stringRefs []string - // nextStateID is the id allocated for the next - // StateOp. - nextStateID uint32 - // multipOp indicates a multi-op such as clip.Path is being added. - multipOp bool - - macroStack stack - stacks [_StackKind]stack -} - -type OpType byte - -type Shape byte - -// Start at a high number for easier debugging. -const firstOpIndex = 200 - -const ( - TypeMacro OpType = iota + firstOpIndex - TypeCall - TypeDefer - TypeTransform - TypePopTransform - TypePushOpacity - TypePopOpacity - TypeImage - TypePaint - TypeColor - TypeLinearGradient - TypePass - TypePopPass - TypeInput - TypeKeyInputHint - TypeSave - TypeLoad - TypeAux - TypeClip - TypePopClip - TypeCursor - TypePath - TypeStroke - TypeSemanticLabel - TypeSemanticDesc - TypeSemanticClass - TypeSemanticSelected - TypeSemanticEnabled - TypeActionInput -) - -type StackID struct { - id uint32 - prev uint32 -} - -// StateOp represents a saved operation snapshot to be restored -// later. -type StateOp struct { - id uint32 - macroID uint32 - ops *Ops -} - -// stack tracks the integer identities of stack operations to ensure correct -// pairing of their push and pop methods. -type stack struct { - currentID uint32 - nextID uint32 -} - -type StackKind uint8 - -// ClipOp is the shadow of clip.Op. -type ClipOp struct { - Bounds image.Rectangle - Outline bool - Shape Shape -} - -const ( - ClipStack StackKind = iota - TransStack - PassStack - OpacityStack - _StackKind -) - -const ( - Path Shape = iota - Ellipse - Rect -) - -const ( - TypeMacroLen = 1 + 4 + 4 - TypeCallLen = 1 + 4 + 4 + 4 + 4 - TypeDeferLen = 1 - TypeTransformLen = 1 + 1 + 4*6 - TypePopTransformLen = 1 - TypePushOpacityLen = 1 + 4 - TypePopOpacityLen = 1 - TypeRedrawLen = 1 + 8 - TypeImageLen = 1 + 1 - TypePaintLen = 1 - TypeColorLen = 1 + 4 - TypeLinearGradientLen = 1 + 8*2 + 4*2 - TypePassLen = 1 - TypePopPassLen = 1 - TypeInputLen = 1 - TypeKeyInputHintLen = 1 + 1 - TypeSaveLen = 1 + 4 - TypeLoadLen = 1 + 4 - TypeAuxLen = 1 - TypeClipLen = 1 + 4*4 + 1 + 1 - TypePopClipLen = 1 - TypeCursorLen = 2 - TypePathLen = 8 + 1 - TypeStrokeLen = 1 + 4 - TypeSemanticLabelLen = 1 - TypeSemanticDescLen = 1 - TypeSemanticClassLen = 2 - TypeSemanticSelectedLen = 2 - TypeSemanticEnabledLen = 2 - TypeActionInputLen = 1 + 1 -) - -func (op *ClipOp) Decode(data []byte) { - if len(data) < TypeClipLen || OpType(data[0]) != TypeClip { - panic("invalid op") - } - data = data[:TypeClipLen] - bo := binary.LittleEndian - op.Bounds.Min.X = int(int32(bo.Uint32(data[1:]))) - op.Bounds.Min.Y = int(int32(bo.Uint32(data[5:]))) - op.Bounds.Max.X = int(int32(bo.Uint32(data[9:]))) - op.Bounds.Max.Y = int(int32(bo.Uint32(data[13:]))) - op.Outline = data[17] == 1 - op.Shape = Shape(data[18]) -} - -func Reset(o *Ops) { - o.macroStack = stack{} - o.stacks = [_StackKind]stack{} - // Leave references to the GC. - for i := range o.refs { - o.refs[i] = nil - } - for i := range o.stringRefs { - o.stringRefs[i] = "" - } - o.data = o.data[:0] - o.refs = o.refs[:0] - o.stringRefs = o.stringRefs[:0] - o.nextStateID = 0 - o.version++ -} - -func Write(o *Ops, n int) []byte { - if o.multipOp { - panic("cannot mix multi ops with single ones") - } - o.data = append(o.data, make([]byte, n)...) - return o.data[len(o.data)-n:] -} - -func BeginMulti(o *Ops) { - if o.multipOp { - panic("cannot interleave multi ops") - } - o.multipOp = true -} - -func EndMulti(o *Ops) { - if !o.multipOp { - panic("cannot end non multi ops") - } - o.multipOp = false -} - -func WriteMulti(o *Ops, n int) []byte { - if !o.multipOp { - panic("cannot use multi ops in single ops") - } - o.data = append(o.data, make([]byte, n)...) - return o.data[len(o.data)-n:] -} - -func PushMacro(o *Ops) StackID { - return o.macroStack.push() -} - -func PopMacro(o *Ops, id StackID) { - o.macroStack.pop(id) -} - -func FillMacro(o *Ops, startPC PC) { - pc := PCFor(o) - // Fill out the macro definition reserved in Record. - data := o.data[startPC.data:] - data = data[:TypeMacroLen] - data[0] = byte(TypeMacro) - bo := binary.LittleEndian - bo.PutUint32(data[1:], uint32(pc.data)) - bo.PutUint32(data[5:], uint32(pc.refs)) -} - -func AddCall(o *Ops, callOps *Ops, pc PC, end PC) { - data := Write1(o, TypeCallLen, callOps) - data[0] = byte(TypeCall) - bo := binary.LittleEndian - bo.PutUint32(data[1:], uint32(pc.data)) - bo.PutUint32(data[5:], uint32(pc.refs)) - bo.PutUint32(data[9:], uint32(end.data)) - bo.PutUint32(data[13:], uint32(end.refs)) -} - -func PushOp(o *Ops, kind StackKind) (StackID, uint32) { - return o.stacks[kind].push(), o.macroStack.currentID -} - -func PopOp(o *Ops, kind StackKind, sid StackID, macroID uint32) { - if o.macroStack.currentID != macroID { - panic("stack push and pop must not cross macro boundary") - } - o.stacks[kind].pop(sid) -} - -func Write1(o *Ops, n int, ref1 any) []byte { - o.data = append(o.data, make([]byte, n)...) - o.refs = append(o.refs, ref1) - return o.data[len(o.data)-n:] -} - -func Write1String(o *Ops, n int, ref1 string) []byte { - o.data = append(o.data, make([]byte, n)...) - o.stringRefs = append(o.stringRefs, ref1) - o.refs = append(o.refs, &o.stringRefs[len(o.stringRefs)-1]) - return o.data[len(o.data)-n:] -} - -func Write2(o *Ops, n int, ref1, ref2 any) []byte { - o.data = append(o.data, make([]byte, n)...) - o.refs = append(o.refs, ref1, ref2) - return o.data[len(o.data)-n:] -} - -func Write2String(o *Ops, n int, ref1 any, ref2 string) []byte { - o.data = append(o.data, make([]byte, n)...) - o.stringRefs = append(o.stringRefs, ref2) - o.refs = append(o.refs, ref1, &o.stringRefs[len(o.stringRefs)-1]) - return o.data[len(o.data)-n:] -} - -func Write3(o *Ops, n int, ref1, ref2, ref3 any) []byte { - o.data = append(o.data, make([]byte, n)...) - o.refs = append(o.refs, ref1, ref2, ref3) - return o.data[len(o.data)-n:] -} - -func PCFor(o *Ops) PC { - return PC{data: uint32(len(o.data)), refs: uint32(len(o.refs))} -} - -func (s *stack) push() StackID { - s.nextID++ - sid := StackID{ - id: s.nextID, - prev: s.currentID, - } - s.currentID = s.nextID - return sid -} - -func (s *stack) check(sid StackID) { - if s.currentID != sid.id { - panic("unbalanced operation") - } -} - -func (s *stack) pop(sid StackID) { - s.check(sid) - s.currentID = sid.prev -} - -// Save the effective transformation. -func Save(o *Ops) StateOp { - o.nextStateID++ - s := StateOp{ - ops: o, - id: o.nextStateID, - macroID: o.macroStack.currentID, - } - bo := binary.LittleEndian - data := Write(o, TypeSaveLen) - data[0] = byte(TypeSave) - bo.PutUint32(data[1:], uint32(s.id)) - return s -} - -// Load a previously saved operations state given -// its ID. -func (s StateOp) Load() { - bo := binary.LittleEndian - data := Write(s.ops, TypeLoadLen) - data[0] = byte(TypeLoad) - bo.PutUint32(data[1:], uint32(s.id)) -} - -func DecodeCommand(d []byte) scene.Command { - var cmd scene.Command - copy(byteslice.Uint32(cmd[:]), d) - return cmd -} - -func EncodeCommand(out []byte, cmd scene.Command) { - copy(out, byteslice.Uint32(cmd[:])) -} - -func DecodeTransform(data []byte) (t f32.Affine2D, push bool) { - if OpType(data[0]) != TypeTransform { - panic("invalid op") - } - push = data[1] != 0 - data = data[2:] - data = data[:4*6] - - bo := binary.LittleEndian - a := math.Float32frombits(bo.Uint32(data)) - b := math.Float32frombits(bo.Uint32(data[4*1:])) - c := math.Float32frombits(bo.Uint32(data[4*2:])) - d := math.Float32frombits(bo.Uint32(data[4*3:])) - e := math.Float32frombits(bo.Uint32(data[4*4:])) - f := math.Float32frombits(bo.Uint32(data[4*5:])) - return f32.NewAffine2D(a, b, c, d, e, f), push -} - -func DecodeOpacity(data []byte) float32 { - if OpType(data[0]) != TypePushOpacity { - panic("invalid op") - } - bo := binary.LittleEndian - return math.Float32frombits(bo.Uint32(data[1:])) -} - -// DecodeSave decodes the state id of a save op. -func DecodeSave(data []byte) int { - if OpType(data[0]) != TypeSave { - panic("invalid op") - } - bo := binary.LittleEndian - return int(bo.Uint32(data[1:])) -} - -// DecodeLoad decodes the state id of a load op. -func DecodeLoad(data []byte) int { - if OpType(data[0]) != TypeLoad { - panic("invalid op") - } - bo := binary.LittleEndian - return int(bo.Uint32(data[1:])) -} - -type opProp struct { - Size byte - NumRefs byte -} - -var opProps = [0x100]opProp{ - TypeMacro: {Size: TypeMacroLen, NumRefs: 0}, - TypeCall: {Size: TypeCallLen, NumRefs: 1}, - TypeDefer: {Size: TypeDeferLen, NumRefs: 0}, - TypeTransform: {Size: TypeTransformLen, NumRefs: 0}, - TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0}, - TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0}, - TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0}, - TypeImage: {Size: TypeImageLen, NumRefs: 2}, - TypePaint: {Size: TypePaintLen, NumRefs: 0}, - TypeColor: {Size: TypeColorLen, NumRefs: 0}, - TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0}, - TypePass: {Size: TypePassLen, NumRefs: 0}, - TypePopPass: {Size: TypePopPassLen, NumRefs: 0}, - TypeInput: {Size: TypeInputLen, NumRefs: 1}, - TypeKeyInputHint: {Size: TypeKeyInputHintLen, NumRefs: 1}, - TypeSave: {Size: TypeSaveLen, NumRefs: 0}, - TypeLoad: {Size: TypeLoadLen, NumRefs: 0}, - TypeAux: {Size: TypeAuxLen, NumRefs: 0}, - TypeClip: {Size: TypeClipLen, NumRefs: 0}, - TypePopClip: {Size: TypePopClipLen, NumRefs: 0}, - TypeCursor: {Size: TypeCursorLen, NumRefs: 0}, - TypePath: {Size: TypePathLen, NumRefs: 0}, - TypeStroke: {Size: TypeStrokeLen, NumRefs: 0}, - TypeSemanticLabel: {Size: TypeSemanticLabelLen, NumRefs: 1}, - TypeSemanticDesc: {Size: TypeSemanticDescLen, NumRefs: 1}, - TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0}, - TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0}, - TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0}, - TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0}, -} - -func (t OpType) props() (size, numRefs uint32) { - v := opProps[t] - return uint32(v.Size), uint32(v.NumRefs) -} - -func (t OpType) Size() uint32 { - return uint32(opProps[t].Size) -} - -func (t OpType) NumRefs() uint32 { - return uint32(opProps[t].NumRefs) -} - -func (t OpType) String() string { - switch t { - case TypeMacro: - return "Macro" - case TypeCall: - return "Call" - case TypeDefer: - return "Defer" - case TypeTransform: - return "Transform" - case TypePopTransform: - return "PopTransform" - case TypePushOpacity: - return "PushOpacity" - case TypePopOpacity: - return "PopOpacity" - case TypeImage: - return "Image" - case TypePaint: - return "Paint" - case TypeColor: - return "Color" - case TypeLinearGradient: - return "LinearGradient" - case TypePass: - return "Pass" - case TypePopPass: - return "PopPass" - case TypeInput: - return "Input" - case TypeKeyInputHint: - return "KeyInputHint" - case TypeSave: - return "Save" - case TypeLoad: - return "Load" - case TypeAux: - return "Aux" - case TypeClip: - return "Clip" - case TypePopClip: - return "PopClip" - case TypeCursor: - return "Cursor" - case TypePath: - return "Path" - case TypeStroke: - return "Stroke" - case TypeSemanticLabel: - return "SemanticDescription" - default: - panic("unknown OpType") - } -} diff --git a/gio/internal/ops/reader.go b/gio/internal/ops/reader.go deleted file mode 100644 index 8964ec6..0000000 --- a/gio/internal/ops/reader.go +++ /dev/null @@ -1,190 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package ops - -import ( - "encoding/binary" -) - -// Reader parses an ops list. -type Reader struct { - pc PC - stack []macro - ops *Ops - deferOps Ops - deferDone bool -} - -// EncodedOp represents an encoded op returned by -// Reader. -type EncodedOp struct { - Key Key - Data []byte - Refs []any -} - -// Key is a unique key for a given op. -type Key struct { - ops *Ops - pc uint32 - version uint32 -} - -// Shadow of op.MacroOp. -type macroOp struct { - ops *Ops - start PC - end PC -} - -// PC is an instruction counter for an operation list. -type PC struct { - data uint32 - refs uint32 -} - -type macro struct { - ops *Ops - retPC PC - endPC PC -} - -type opMacroDef struct { - endpc PC -} - -func (pc PC) Add(op OpType) PC { - size, numRefs := op.props() - return PC{ - data: pc.data + size, - refs: pc.refs + numRefs, - } -} - -// Reset start reading from the beginning of ops. -func (r *Reader) Reset(ops *Ops) { - r.ResetAt(ops, PC{}) -} - -// ResetAt is like Reset, except it starts reading from pc. -func (r *Reader) ResetAt(ops *Ops, pc PC) { - r.stack = r.stack[:0] - Reset(&r.deferOps) - r.deferDone = false - r.pc = pc - r.ops = ops -} - -func (r *Reader) Decode() (EncodedOp, bool) { - if r.ops == nil { - return EncodedOp{}, false - } - deferring := false - for { - if len(r.stack) > 0 { - b := r.stack[len(r.stack)-1] - if r.pc == b.endPC { - r.ops = b.ops - r.pc = b.retPC - r.stack = r.stack[:len(r.stack)-1] - continue - } - } - data := r.ops.data - data = data[r.pc.data:] - refs := r.ops.refs - if len(data) == 0 { - if r.deferDone { - return EncodedOp{}, false - } - r.deferDone = true - // Execute deferred macros. - r.ops = &r.deferOps - r.pc = PC{} - continue - } - key := Key{ops: r.ops, pc: r.pc.data, version: r.ops.version} - t := OpType(data[0]) - n, nrefs := t.props() - data = data[:n] - refs = refs[r.pc.refs:] - refs = refs[:nrefs] - switch t { - case TypeDefer: - deferring = true - r.pc.data += n - r.pc.refs += nrefs - continue - case TypeAux: - // An Aux operations is always wrapped in a macro, and - // its length is the remaining space. - block := r.stack[len(r.stack)-1] - n += block.endPC.data - r.pc.data - TypeAuxLen - data = data[:n] - case TypeCall: - if deferring { - deferring = false - // Copy macro for deferred execution. - if nrefs != 1 { - panic("internal error: unexpected number of macro refs") - } - deferData := Write1(&r.deferOps, int(n), refs[0]) - copy(deferData, data) - r.pc.data += n - r.pc.refs += nrefs - continue - } - var op macroOp - op.decode(data, refs) - retPC := r.pc - retPC.data += n - retPC.refs += nrefs - r.stack = append(r.stack, macro{ - ops: r.ops, - retPC: retPC, - endPC: op.end, - }) - r.ops = op.ops - r.pc = op.start - continue - case TypeMacro: - var op opMacroDef - op.decode(data) - if op.endpc != (PC{}) { - r.pc = op.endpc - } else { - // Treat an incomplete macro as containing all remaining ops. - r.pc.data = uint32(len(r.ops.data)) - r.pc.refs = uint32(len(r.ops.refs)) - } - continue - } - r.pc.data += n - r.pc.refs += nrefs - return EncodedOp{Key: key, Data: data, Refs: refs}, true - } -} - -func (op *opMacroDef) decode(data []byte) { - if len(data) < TypeMacroLen || OpType(data[0]) != TypeMacro { - panic("invalid op") - } - bo := binary.LittleEndian - data = data[:TypeMacroLen] - op.endpc.data = bo.Uint32(data[1:]) - op.endpc.refs = bo.Uint32(data[5:]) -} - -func (m *macroOp) decode(data []byte, refs []any) { - if len(data) < TypeCallLen || len(refs) < 1 || OpType(data[0]) != TypeCall { - panic("invalid op") - } - bo := binary.LittleEndian - data = data[:TypeCallLen] - - m.ops = refs[0].(*Ops) - m.start.data = bo.Uint32(data[1:]) - m.start.refs = bo.Uint32(data[5:]) - m.end.data = bo.Uint32(data[9:]) - m.end.refs = bo.Uint32(data[13:]) -} diff --git a/gio/internal/scene/scene.go b/gio/internal/scene/scene.go deleted file mode 100644 index d4de38b..0000000 --- a/gio/internal/scene/scene.go +++ /dev/null @@ -1,251 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package scene encodes and decodes graphics commands in the format used by the -// compute renderer. -package scene - -import ( - "fmt" - "image" - "image/color" - "math" - "unsafe" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32" -) - -type Op uint32 - -type Command [sceneElemSize / 4]uint32 - -// GPU commands from piet/scene.h in package gioui.org/shaders. -const ( - OpNop Op = iota - OpLine - OpQuad - OpCubic - OpFillColor - OpLineWidth - OpTransform - OpBeginClip - OpEndClip - OpFillImage - OpSetFillMode - OpGap -) - -// FillModes, from setup.h. -type FillMode uint32 - -const ( - FillModeNonzero = 0 - FillModeStroke = 1 -) - -const CommandSize = int(unsafe.Sizeof(Command{})) - -const sceneElemSize = 36 - -func (c Command) Op() Op { - return Op(c[0]) -} - -func (c Command) String() string { - switch Op(c[0]) { - case OpNop: - return "nop" - case OpLine: - from, to := DecodeLine(c) - return fmt.Sprintf("line(%v, %v)", from, to) - case OpGap: - from, to := DecodeLine(c) - return fmt.Sprintf("gap(%v, %v)", from, to) - case OpQuad: - from, ctrl, to := DecodeQuad(c) - return fmt.Sprintf("quad(%v, %v, %v)", from, ctrl, to) - case OpCubic: - from, ctrl0, ctrl1, to := DecodeCubic(c) - return fmt.Sprintf("cubic(%v, %v, %v, %v)", from, ctrl0, ctrl1, to) - case OpFillColor: - return fmt.Sprintf("fillcolor %#.8x", c[1]) - case OpLineWidth: - return "linewidth" - case OpTransform: - t := f32.NewAffine2D( - math.Float32frombits(c[1]), - math.Float32frombits(c[3]), - math.Float32frombits(c[5]), - math.Float32frombits(c[2]), - math.Float32frombits(c[4]), - math.Float32frombits(c[6]), - ) - return fmt.Sprintf("transform (%v)", t) - case OpBeginClip: - bounds := f32.Rectangle{ - Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])), - Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])), - } - return fmt.Sprintf("beginclip (%v)", bounds) - case OpEndClip: - bounds := f32.Rectangle{ - Min: f32.Pt(math.Float32frombits(c[1]), math.Float32frombits(c[2])), - Max: f32.Pt(math.Float32frombits(c[3]), math.Float32frombits(c[4])), - } - return fmt.Sprintf("endclip (%v)", bounds) - case OpFillImage: - return "fillimage" - case OpSetFillMode: - return "setfillmode" - default: - panic("unreachable") - } -} - -func Line(start, end f32.Point) Command { - return Command{ - 0: uint32(OpLine), - 1: math.Float32bits(start.X), - 2: math.Float32bits(start.Y), - 3: math.Float32bits(end.X), - 4: math.Float32bits(end.Y), - } -} - -func Gap(start, end f32.Point) Command { - return Command{ - 0: uint32(OpGap), - 1: math.Float32bits(start.X), - 2: math.Float32bits(start.Y), - 3: math.Float32bits(end.X), - 4: math.Float32bits(end.Y), - } -} - -func Cubic(start, ctrl0, ctrl1, end f32.Point) Command { - return Command{ - 0: uint32(OpCubic), - 1: math.Float32bits(start.X), - 2: math.Float32bits(start.Y), - 3: math.Float32bits(ctrl0.X), - 4: math.Float32bits(ctrl0.Y), - 5: math.Float32bits(ctrl1.X), - 6: math.Float32bits(ctrl1.Y), - 7: math.Float32bits(end.X), - 8: math.Float32bits(end.Y), - } -} - -func Quad(start, ctrl, end f32.Point) Command { - return Command{ - 0: uint32(OpQuad), - 1: math.Float32bits(start.X), - 2: math.Float32bits(start.Y), - 3: math.Float32bits(ctrl.X), - 4: math.Float32bits(ctrl.Y), - 5: math.Float32bits(end.X), - 6: math.Float32bits(end.Y), - } -} - -func Transform(m f32.Affine2D) Command { - sx, hx, ox, hy, sy, oy := m.Elems() - return Command{ - 0: uint32(OpTransform), - 1: math.Float32bits(sx), - 2: math.Float32bits(hy), - 3: math.Float32bits(hx), - 4: math.Float32bits(sy), - 5: math.Float32bits(ox), - 6: math.Float32bits(oy), - } -} - -func SetLineWidth(width float32) Command { - return Command{ - 0: uint32(OpLineWidth), - 1: math.Float32bits(width), - } -} - -func BeginClip(bbox f32.Rectangle) Command { - return Command{ - 0: uint32(OpBeginClip), - 1: math.Float32bits(bbox.Min.X), - 2: math.Float32bits(bbox.Min.Y), - 3: math.Float32bits(bbox.Max.X), - 4: math.Float32bits(bbox.Max.Y), - } -} - -func EndClip(bbox f32.Rectangle) Command { - return Command{ - 0: uint32(OpEndClip), - 1: math.Float32bits(bbox.Min.X), - 2: math.Float32bits(bbox.Min.Y), - 3: math.Float32bits(bbox.Max.X), - 4: math.Float32bits(bbox.Max.Y), - } -} - -func FillColor(col color.RGBA) Command { - return Command{ - 0: uint32(OpFillColor), - 1: uint32(col.R)<<24 | uint32(col.G)<<16 | uint32(col.B)<<8 | uint32(col.A), - } -} - -func FillImage(index int, offset image.Point) Command { - x := int16(offset.X) - y := int16(offset.Y) - return Command{ - 0: uint32(OpFillImage), - 1: uint32(index), - 2: uint32(uint16(x)) | uint32(uint16(y))<<16, - } -} - -func SetFillMode(mode FillMode) Command { - return Command{ - 0: uint32(OpSetFillMode), - 1: uint32(mode), - } -} - -func DecodeLine(cmd Command) (from, to f32.Point) { - if cmd[0] != uint32(OpLine) { - panic("invalid command") - } - from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) - to = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) - return -} - -func DecodeGap(cmd Command) (from, to f32.Point) { - if cmd[0] != uint32(OpGap) { - panic("invalid command") - } - from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) - to = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) - return -} - -func DecodeQuad(cmd Command) (from, ctrl, to f32.Point) { - if cmd[0] != uint32(OpQuad) { - panic("invalid command") - } - from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) - ctrl = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) - to = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6])) - return -} - -func DecodeCubic(cmd Command) (from, ctrl0, ctrl1, to f32.Point) { - if cmd[0] != uint32(OpCubic) { - panic("invalid command") - } - from = f32.Pt(math.Float32frombits(cmd[1]), math.Float32frombits(cmd[2])) - ctrl0 = f32.Pt(math.Float32frombits(cmd[3]), math.Float32frombits(cmd[4])) - ctrl1 = f32.Pt(math.Float32frombits(cmd[5]), math.Float32frombits(cmd[6])) - to = f32.Pt(math.Float32frombits(cmd[7]), math.Float32frombits(cmd[8])) - return -} diff --git a/gio/internal/stroke/stroke.go b/gio/internal/stroke/stroke.go deleted file mode 100644 index be700ef..0000000 --- a/gio/internal/stroke/stroke.go +++ /dev/null @@ -1,760 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Most of the algorithms to compute strokes and their offsets have been -// extracted, adapted from (and used as a reference implementation): -// - github.com/tdewolff/canvas (Licensed under MIT) -// -// These algorithms have been implemented from: -// Fast, precise flattening of cubic Bézier path and offset curves -// Thomas F. Hain, et al. -// -// An electronic version is available at: -// https://seant23.files.wordpress.com/2010/11/fastpreciseflatteningofbeziercurve.pdf -// -// Possible improvements (in term of speed and/or accuracy) on these -// algorithms are: -// -// - Polar Stroking: New Theory and Methods for Stroking Paths, -// M. Kilgard -// https://arxiv.org/pdf/2007.00308.pdf -// -// - https://raphlinus.github.io/graphics/curves/2019/12/23/flatten-quadbez.html -// R. Levien - -// Package stroke implements conversion of strokes to filled outlines. It is used as a -// fallback for stroke configurations not natively supported by the renderer. -package stroke - -import ( - "encoding/binary" - "math" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/internal/scene" -) - -// The following are copies of types from op/clip to avoid a circular import of -// that package. -// TODO: when the old renderer is gone, this package can be merged with -// op/clip, eliminating the duplicate types. -type StrokeStyle struct { - Width float32 -} - -// strokeTolerance is used to reconcile rounding errors arising -// when splitting quads into smaller and smaller segments to approximate -// them into straight lines, and when joining back segments. -// -// The magic value of 0.01 was found by striking a compromise between -// aesthetic looking (curves did look like curves, even after linearization) -// and speed. -const strokeTolerance = 0.01 - -type QuadSegment struct { - From, Ctrl, To f32.Point -} - -type StrokeQuad struct { - Contour uint32 - Quad QuadSegment -} - -type strokeState struct { - p0, p1 f32.Point // p0 is the start point, p1 the end point. - n0, n1 f32.Point // n0 is the normal vector at the start point, n1 at the end point. - r0, r1 float32 // r0 is the curvature at the start point, r1 at the end point. - ctl f32.Point // ctl is the control point of the quadratic Bézier segment. -} - -type StrokeQuads []StrokeQuad - -func (qs *StrokeQuads) pen() f32.Point { - return (*qs)[len(*qs)-1].Quad.To -} - -func (qs *StrokeQuads) lineTo(pt f32.Point) { - end := qs.pen() - *qs = append(*qs, StrokeQuad{ - Quad: QuadSegment{ - From: end, - Ctrl: end.Add(pt).Mul(0.5), - To: pt, - }, - }) -} - -func (qs *StrokeQuads) arc(f1, f2 f32.Point, angle float32) { - pen := qs.pen() - m, segments := ArcTransform(pen, f1.Add(pen), f2.Add(pen), angle) - for range segments { - p0 := qs.pen() - p1 := m.Transform(p0) - p2 := m.Transform(p1) - ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) - *qs = append(*qs, StrokeQuad{ - Quad: QuadSegment{ - From: p0, Ctrl: ctl, To: p2, - }, - }) - } -} - -// split splits a slice of quads into slices of quads grouped -// by contours (ie: splitted at move-to boundaries). -func (qs StrokeQuads) split() []StrokeQuads { - if len(qs) == 0 { - return nil - } - - var ( - c uint32 - o []StrokeQuads - i = len(o) - ) - for _, q := range qs { - if q.Contour != c { - c = q.Contour - i = len(o) - o = append(o, StrokeQuads{}) - } - o[i] = append(o[i], q) - } - - return o -} - -func (qs StrokeQuads) stroke(stroke StrokeStyle) StrokeQuads { - var ( - o StrokeQuads - hw = 0.5 * stroke.Width - ) - - for _, ps := range qs.split() { - rhs, lhs := ps.offset(hw, stroke) - switch lhs { - case nil: - o = o.append(rhs) - default: - // Closed path. - // Inner path should go opposite direction to cancel outer path. - switch { - case ps.ccw(): - lhs = lhs.reverse() - o = o.append(rhs) - o = o.append(lhs) - default: - rhs = rhs.reverse() - o = o.append(lhs) - o = o.append(rhs) - } - } - } - - return o -} - -// offset returns the right-hand and left-hand sides of the path, offset by -// the half-width hw. -// The stroke handles how segments are joined and ends are capped. -func (qs StrokeQuads) offset(hw float32, stroke StrokeStyle) (rhs, lhs StrokeQuads) { - var ( - states []strokeState - beg = qs[0].Quad.From - end = qs[len(qs)-1].Quad.To - closed = beg == end - ) - for i := range qs { - q := qs[i].Quad - - var ( - n0 = strokePathNorm(q.From, q.Ctrl, q.To, 0, hw) - n1 = strokePathNorm(q.From, q.Ctrl, q.To, 1, hw) - r0 = strokePathCurv(q.From, q.Ctrl, q.To, 0) - r1 = strokePathCurv(q.From, q.Ctrl, q.To, 1) - ) - states = append(states, strokeState{ - p0: q.From, - p1: q.To, - n0: n0, - n1: n1, - r0: r0, - r1: r1, - ctl: q.Ctrl, - }) - } - - for i, state := range states { - rhs = rhs.append(strokeQuadBezier(state, +hw, strokeTolerance)) - lhs = lhs.append(strokeQuadBezier(state, -hw, strokeTolerance)) - - // join the current and next segments - if hasNext := i+1 < len(states); hasNext || closed { - var next strokeState - switch { - case hasNext: - next = states[i+1] - case closed: - next = states[0] - } - if state.n1 != next.n0 { - strokePathRoundJoin(&rhs, &lhs, hw, state.p1, state.n1, next.n0, state.r1, next.r0) - } - } - } - - if closed { - rhs.close() - lhs.close() - return rhs, lhs - } - - qbeg := &states[0] - qend := &states[len(states)-1] - - // Default to counter-clockwise direction. - lhs = lhs.reverse() - strokePathCap(stroke, &rhs, hw, qend.p1, qend.n1) - - rhs = rhs.append(lhs) - strokePathCap(stroke, &rhs, hw, qbeg.p0, qbeg.n0.Mul(-1)) - - rhs.close() - - return rhs, nil -} - -func (qs *StrokeQuads) close() { - p0 := (*qs)[len(*qs)-1].Quad.To - p1 := (*qs)[0].Quad.From - - if p1 == p0 { - return - } - - *qs = append(*qs, StrokeQuad{ - Quad: QuadSegment{ - From: p0, - Ctrl: p0.Add(p1).Mul(0.5), - To: p1, - }, - }) -} - -// ccw returns whether the path is counter-clockwise. -func (qs StrokeQuads) ccw() bool { - // Use the Shoelace formula: - // https://en.wikipedia.org/wiki/Shoelace_formula - var area float32 - for _, ps := range qs.split() { - for i := 1; i < len(ps); i++ { - pi := ps[i].Quad.To - pj := ps[i-1].Quad.To - area += (pi.X - pj.X) * (pi.Y + pj.Y) - } - } - return area <= 0.0 -} - -func (qs StrokeQuads) reverse() StrokeQuads { - if len(qs) == 0 { - return nil - } - - ps := make(StrokeQuads, 0, len(qs)) - for i := range qs { - q := qs[len(qs)-1-i] - q.Quad.To, q.Quad.From = q.Quad.From, q.Quad.To - ps = append(ps, q) - } - - return ps -} - -func (qs StrokeQuads) append(ps StrokeQuads) StrokeQuads { - switch { - case len(ps) == 0: - return qs - case len(qs) == 0: - return ps - } - - // Consolidate quads and smooth out rounding errors. - // We need to also check for the strokeTolerance to correctly handle - // join/cap points or on-purpose disjoint quads. - p0 := qs[len(qs)-1].Quad.To - p1 := ps[0].Quad.From - if p0 != p1 && lenPt(p0.Sub(p1)) < strokeTolerance { - qs = append(qs, StrokeQuad{ - Quad: QuadSegment{ - From: p0, - Ctrl: p0.Add(p1).Mul(0.5), - To: p1, - }, - }) - } - return append(qs, ps...) -} - -func (q QuadSegment) Transform(t f32.Affine2D) QuadSegment { - q.From = t.Transform(q.From) - q.Ctrl = t.Transform(q.Ctrl) - q.To = t.Transform(q.To) - return q -} - -// strokePathNorm returns the normal vector at t. -func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point { - switch t { - case 0: - n := p1.Sub(p0) - if n.X == 0 && n.Y == 0 { - return f32.Point{} - } - n = rot90CW(n) - return normPt(n, d) - case 1: - n := p2.Sub(p1) - if n.X == 0 && n.Y == 0 { - return f32.Point{} - } - n = rot90CW(n) - return normPt(n, d) - } - panic("impossible") -} - -func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) } - -func normPt(p f32.Point, l float32) f32.Point { - if (p.X == 0 && p.Y == 0) || l == 0 { - return f32.Point{} - } - isVerticalUnit := p.X == 0 && (p.Y == l || p.Y == -l) - isHorizontalUnit := p.Y == 0 && (p.X == l || p.X == -l) - if isVerticalUnit || isHorizontalUnit { - if math.Signbit(float64(l)) { - return f32.Point{X: -p.X, Y: -p.Y} - } else { - return f32.Point{X: p.X, Y: p.Y} - } - } - d := math.Hypot(float64(p.X), float64(p.Y)) - l64 := float64(l) - if math.Abs(d-l64) < 1e-10 { - if math.Signbit(float64(l)) { - return f32.Point{X: -p.X, Y: -p.Y} - } else { - return f32.Point{X: p.X, Y: p.Y} - } - } - n := float32(l64 / d) - return f32.Point{X: p.X * n, Y: p.Y * n} -} - -func lenPt(p f32.Point) float32 { - return float32(math.Hypot(float64(p.X), float64(p.Y))) -} - -func perpDot(p, q f32.Point) float32 { - return p.X*q.Y - p.Y*q.X -} - -func angleBetween(n0, n1 f32.Point) float64 { - return math.Atan2(float64(n1.Y), float64(n1.X)) - - math.Atan2(float64(n0.Y), float64(n0.X)) -} - -// strokePathCurv returns the curvature at t, along the quadratic Bézier -// curve defined by the triplet (beg, ctl, end). -func strokePathCurv(beg, ctl, end f32.Point, t float32) float32 { - var ( - d1p = quadBezierD1(beg, ctl, end, t) - d2p = quadBezierD2(beg, ctl, end, t) - - // Negative when bending right, ie: the curve is CW at this point. - a = float64(perpDot(d1p, d2p)) - ) - - // We check early that the segment isn't too line-like and - // save a costly call to math.Pow that will be discarded by dividing - // with a too small 'a'. - if math.Abs(a) < 1e-10 { - return float32(math.NaN()) - } - return float32(math.Pow(float64(d1p.X*d1p.X+d1p.Y*d1p.Y), 1.5) / a) -} - -// quadBezierSample returns the point on the Bézier curve at t. -// -// B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2 -func quadBezierSample(p0, p1, p2 f32.Point, t float32) f32.Point { - t1 := 1 - t - c0 := t1 * t1 - c1 := 2 * t1 * t - c2 := t * t - - o := p0.Mul(c0) - o = o.Add(p1.Mul(c1)) - o = o.Add(p2.Mul(c2)) - return o -} - -// quadBezierD1 returns the first derivative of the Bézier curve with respect to t. -// -// B'(t) = 2(1-t)(P1 - P0) + 2t(P2 - P1) -func quadBezierD1(p0, p1, p2 f32.Point, t float32) f32.Point { - p10 := p1.Sub(p0).Mul(2 * (1 - t)) - p21 := p2.Sub(p1).Mul(2 * t) - - return p10.Add(p21) -} - -// quadBezierD2 returns the second derivative of the Bézier curve with respect to t: -// -// B''(t) = 2(P2 - 2P1 + P0) -func quadBezierD2(p0, p1, p2 f32.Point, t float32) f32.Point { - p := p2.Sub(p1.Mul(2)).Add(p0) - return p.Mul(2) -} - -func strokeQuadBezier(state strokeState, d, flatness float32) StrokeQuads { - // Gio strokes are only quadratic Bézier curves, w/o any inflection point. - // So we just have to flatten them. - var qs StrokeQuads - return flattenQuadBezier(qs, state.p0, state.ctl, state.p1, d, flatness) -} - -// flattenQuadBezier splits a Bézier quadratic curve into linear sub-segments, -// themselves also encoded as Bézier (degenerate, flat) quadratic curves. -func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, d, flatness float32) StrokeQuads { - var ( - t float32 - flat64 = float64(flatness) - ) - for t < 1 { - s2 := float64((p2.X-p0.X)*(p1.Y-p0.Y) - (p2.Y-p0.Y)*(p1.X-p0.X)) - den := math.Hypot(float64(p1.X-p0.X), float64(p1.Y-p0.Y)) - if s2*den == 0.0 { - break - } - - s2 /= den - t = 2.0 * float32(math.Sqrt(flat64/3.0/math.Abs(s2))) - if t >= 1.0 { - break - } - var q0, q1, q2 f32.Point - q0, q1, q2, p0, p1, p2 = quadBezierSplit(p0, p1, p2, t) - qs.addLine(q0, q1, q2, 0, d) - } - qs.addLine(p0, p1, p2, 1, d) - return qs -} - -func (qs *StrokeQuads) addLine(p0, ctrl, p1 f32.Point, t, d float32) { - switch i := len(*qs); i { - case 0: - p0 = p0.Add(strokePathNorm(p0, ctrl, p1, 0, d)) - default: - // Address possible rounding errors and use previous point. - p0 = (*qs)[i-1].Quad.To - } - - p1 = p1.Add(strokePathNorm(p0, ctrl, p1, 1, d)) - - *qs = append(*qs, - StrokeQuad{ - Quad: QuadSegment{ - From: p0, - Ctrl: p0.Add(p1).Mul(0.5), - To: p1, - }, - }, - ) -} - -// quadInterp returns the interpolated point at t. -func quadInterp(p, q f32.Point, t float32) f32.Point { - return f32.Pt( - (1-t)*p.X+t*q.X, - (1-t)*p.Y+t*q.Y, - ) -} - -// quadBezierSplit returns the pair of triplets (from,ctrl,to) Bézier curve, -// split before (resp. after) the provided parametric t value. -func quadBezierSplit(p0, p1, p2 f32.Point, t float32) (f32.Point, f32.Point, f32.Point, f32.Point, f32.Point, f32.Point) { - var ( - b0 = p0 - b1 = quadInterp(p0, p1, t) - b2 = quadBezierSample(p0, p1, p2, t) - - a0 = b2 - a1 = quadInterp(p1, p2, t) - a2 = p2 - ) - - return b0, b1, b2, a0, a1, a2 -} - -// strokePathRoundJoin joins the two paths rhs and lhs, creating an arc. -func strokePathRoundJoin(rhs, lhs *StrokeQuads, hw float32, pivot, n0, n1 f32.Point, r0, r1 float32) { - rp := pivot.Add(n1) - lp := pivot.Sub(n1) - angle := angleBetween(n0, n1) - switch { - case angle <= 0: - // Path bends to the right, ie. CW (or 180 degree turn). - c := pivot.Sub(lhs.pen()) - lhs.arc(c, c, float32(angle)) - lhs.lineTo(lp) // Add a line to accommodate for rounding errors. - rhs.lineTo(rp) - default: - // Path bends to the left, ie. CCW. - c := pivot.Sub(rhs.pen()) - rhs.arc(c, c, float32(angle)) - rhs.lineTo(rp) // Add a line to accommodate for rounding errors. - lhs.lineTo(lp) - } -} - -// strokePathCap caps the provided path qs, according to the provided stroke operation. -func strokePathCap(stroke StrokeStyle, qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { - strokePathRoundCap(qs, hw, pivot, n0) -} - -// strokePathRoundCap caps the start or end of a path with a round cap. -func strokePathRoundCap(qs *StrokeQuads, hw float32, pivot, n0 f32.Point) { - c := pivot.Sub(qs.pen()) - qs.arc(c, c, math.Pi) -} - -// ArcTransform computes a transformation that can be used for generating quadratic bézier -// curve approximations for an arc. -// -// The math is extracted from the following paper: -// -// "Drawing an elliptical arc using polylines, quadratic or -// cubic Bezier curves", L. Maisonobe -// -// An electronic version may be found at: -// -// http://spaceroots.org/documents/ellipse/elliptical-arc.pdf -func ArcTransform(p, f1, f2 f32.Point, angle float32) (transform f32.Affine2D, segments int) { - const segmentsPerCircle = 16 - const anglePerSegment = 2 * math.Pi / segmentsPerCircle - - s := angle / anglePerSegment - if s < 0 { - s = -s - } - segments = int(math.Ceil(float64(s))) - if segments <= 0 { - segments = 1 - } - - var rx, ry, alpha float64 - if f1 == f2 { - // degenerate case of a circle. - rx = dist(f1, p) - ry = rx - } else { - // semi-major axis: 2a = |PF1| + |PF2| - a := 0.5 * (dist(f1, p) + dist(f2, p)) - // semi-minor axis: c^2 = a^2 - b^2 (c: focal distance) - c := dist(f1, f2) * 0.5 - b := math.Sqrt(a*a - c*c) - switch { - case a > b: - rx = a - ry = b - default: - rx = b - ry = a - } - if f1.X == f2.X { - // special case of a "vertical" ellipse. - alpha = math.Pi / 2 - if f1.Y < f2.Y { - alpha = -alpha - } - } else { - x := float64(f1.X-f2.X) * 0.5 - if x < 0 { - x = -x - } - alpha = math.Acos(x / c) - } - } - - θ := angle / float32(segments) - ref := f32.AffineId() // transform from absolute frame to ellipse-based one - rot := f32.AffineId() // rotation matrix for each segment - inv := f32.AffineId() // transform from ellipse-based frame to absolute one - center := f32.Point{ - X: 0.5 * (f1.X + f2.X), - Y: 0.5 * (f1.Y + f2.Y), - } - ref = ref.Offset(f32.Point{}.Sub(center)) - ref = ref.Rotate(f32.Point{}, float32(-alpha)) - ref = ref.Scale(f32.Point{}, f32.Point{ - X: float32(1 / rx), - Y: float32(1 / ry), - }) - inv = ref.Invert() - rot = rot.Rotate(f32.Point{}, 0.5*θ) - - // Instead of invoking math.Sincos for every segment, compute a rotation - // matrix once and apply for each segment. - // Before applying the rotation matrix rot, transform the coordinates - // to a frame centered to the ellipse (and warped into a unit circle), then rotate. - // Finally, transform back into the original frame. - return inv.Mul(rot).Mul(ref), segments -} - -func dist(p1, p2 f32.Point) float64 { - var ( - x1 = float64(p1.X) - y1 = float64(p1.Y) - x2 = float64(p2.X) - y2 = float64(p2.Y) - dx = x2 - x1 - dy = y2 - y1 - ) - return math.Hypot(dx, dy) -} - -func StrokePathCommands(style StrokeStyle, scene []byte) StrokeQuads { - quads := decodeToStrokeQuads(scene) - return quads.stroke(style) -} - -// decodeToStrokeQuads decodes scene commands to quads ready to stroke. -func decodeToStrokeQuads(pathData []byte) StrokeQuads { - quads := make(StrokeQuads, 0, 2*len(pathData)/(scene.CommandSize+4)) - scratch := make([]QuadSegment, 0, 10) - for len(pathData) >= scene.CommandSize+4 { - contour := binary.LittleEndian.Uint32(pathData) - cmd := ops.DecodeCommand(pathData[4:]) - switch cmd.Op() { - case scene.OpLine: - var q QuadSegment - q.From, q.To = scene.DecodeLine(cmd) - q.Ctrl = q.From.Add(q.To).Mul(.5) - quad := StrokeQuad{ - Contour: contour, - Quad: q, - } - quads = append(quads, quad) - case scene.OpGap: - // Ignore gaps for strokes. - case scene.OpQuad: - var q QuadSegment - q.From, q.Ctrl, q.To = scene.DecodeQuad(cmd) - quad := StrokeQuad{ - Contour: contour, - Quad: q, - } - quads = append(quads, quad) - case scene.OpCubic: - from, ctrl0, ctrl1, to := scene.DecodeCubic(cmd) - scratch = SplitCubic(from, ctrl0, ctrl1, to, scratch[:0]) - for _, q := range scratch { - quad := StrokeQuad{ - Contour: contour, - Quad: q, - } - quads = append(quads, quad) - } - default: - panic("unsupported scene command") - } - pathData = pathData[scene.CommandSize+4:] - } - return quads -} - -func SplitCubic(from, ctrl0, ctrl1, to f32.Point, quads []QuadSegment) []QuadSegment { - // Set the maximum distance proportionally to the longest side - // of the bounding rectangle. - hull := f32.Rectangle{ - Min: from, - Max: ctrl0, - }.Canon().Union(f32.Rectangle{ - Min: ctrl1, - Max: to, - }.Canon()) - l := hull.Dx() - if h := hull.Dy(); h > l { - l = h - } - maxDist := l * 0.001 - approxCubeTo(&quads, 0, maxDist*maxDist, from, ctrl0, ctrl1, to) - return quads -} - -// approxCubeTo approximates a cubic Bézier by a series of quadratic -// curves. -func approxCubeTo(quads *[]QuadSegment, splits int, maxDistSq float32, from, ctrl0, ctrl1, to f32.Point) int { - // The idea is from - // https://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html - // where a quadratic approximates a cubic by eliminating its t³ term - // from its polynomial expression anchored at the starting point: - // - // P(t) = pen + 3t(ctrl0 - pen) + 3t²(ctrl1 - 2ctrl0 + pen) + t³(to - 3ctrl1 + 3ctrl0 - pen) - // - // The control point for the new quadratic Q1 that shares starting point, pen, with P is - // - // C1 = (3ctrl0 - pen)/2 - // - // The reverse cubic anchored at the end point has the polynomial - // - // P'(t) = to + 3t(ctrl1 - to) + 3t²(ctrl0 - 2ctrl1 + to) + t³(pen - 3ctrl0 + 3ctrl1 - to) - // - // The corresponding quadratic Q2 that shares the end point, to, with P has control - // point - // - // C2 = (3ctrl1 - to)/2 - // - // The combined quadratic Bézier, Q, shares both start and end points with its cubic - // and use the midpoint between the two curves Q1 and Q2 as control point: - // - // C = (3ctrl0 - pen + 3ctrl1 - to)/4 - // using, q0 := 3ctrl0 - pen, q1 := 3ctrl1 - to - // C = (q0 + q1)/4 - q0 := ctrl0.Mul(3).Sub(from) - q1 := ctrl1.Mul(3).Sub(to) - c := q0.Add(q1).Mul(1.0 / 4.0) - const maxSplits = 32 - if splits >= maxSplits { - *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) - return splits - } - // The maximum distance between the cubic P and its approximation Q given t - // can be shown to be - // - // d = sqrt(3)/36 * |to - 3ctrl1 + 3ctrl0 - pen| - // reusing, q0 := 3ctrl0 - pen, q1 := 3ctrl1 - to - // d = sqrt(3)/36 * |-q1 + q0| - // - // To save a square root, compare d² with the squared tolerance. - v := q0.Sub(q1) - d2 := (v.X*v.X + v.Y*v.Y) * 3 / (36 * 36) - if d2 <= maxDistSq { - *quads = append(*quads, QuadSegment{From: from, Ctrl: c, To: to}) - return splits - } - // De Casteljau split the curve and approximate the halves. - t := float32(0.5) - c0 := from.Add(ctrl0.Sub(from).Mul(t)) - c1 := ctrl0.Add(ctrl1.Sub(ctrl0).Mul(t)) - c2 := ctrl1.Add(to.Sub(ctrl1).Mul(t)) - c01 := c0.Add(c1.Sub(c0).Mul(t)) - c12 := c1.Add(c2.Sub(c1).Mul(t)) - c0112 := c01.Add(c12.Sub(c01).Mul(t)) - splits++ - splits = approxCubeTo(quads, splits, maxDistSq, from, c0, c01, c0112) - splits = approxCubeTo(quads, splits, maxDistSq, c0112, c12, c2, to) - return splits -} diff --git a/gio/internal/stroke/stroke_test.go b/gio/internal/stroke/stroke_test.go deleted file mode 100644 index 3e6dafa..0000000 --- a/gio/internal/stroke/stroke_test.go +++ /dev/null @@ -1,171 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package stroke - -import ( - "strconv" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32" -) - -func TestNormPt(t *testing.T) { - type scenario struct { - l float32 - ptIn, ptOut f32.Point - } - - scenarios := []scenario{ - // l!=0 && X=Y=0 - {l: 10, ptIn: f32.Point{X: 0, Y: 0}, ptOut: f32.Point{X: 0, Y: 0}}, - {l: -10, ptIn: f32.Point{X: 0, Y: 0}, ptOut: f32.Point{X: 0, Y: 0}}, - - // l>0 & X - {l: +20, ptIn: f32.Point{X: +30, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}}, - {l: +20, ptIn: f32.Point{X: +20, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}}, - {l: +20, ptIn: f32.Point{X: +10, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}}, - {l: +20, ptIn: f32.Point{X: -10, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}}, - {l: +20, ptIn: f32.Point{X: -20, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}}, - {l: +20, ptIn: f32.Point{X: -30, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}}, - - // l<0 & X - {l: -20, ptIn: f32.Point{X: +30, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}}, - {l: -20, ptIn: f32.Point{X: +20, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}}, - {l: -20, ptIn: f32.Point{X: +10, Y: 0}, ptOut: f32.Point{X: -20, Y: 0}}, - {l: -20, ptIn: f32.Point{X: -10, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}}, - {l: -20, ptIn: f32.Point{X: -20, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}}, - {l: -20, ptIn: f32.Point{X: -30, Y: 0}, ptOut: f32.Point{X: +20, Y: 0}}, - - // l>0 & Y - {l: +20, ptIn: f32.Point{X: 0, Y: +30}, ptOut: f32.Point{X: 0, Y: +20}}, - {l: +20, ptIn: f32.Point{X: 0, Y: +20}, ptOut: f32.Point{X: 0, Y: +20}}, - {l: +20, ptIn: f32.Point{X: 0, Y: +10}, ptOut: f32.Point{X: 0, Y: +20}}, - {l: +20, ptIn: f32.Point{X: 0, Y: -10}, ptOut: f32.Point{X: 0, Y: -20}}, - {l: +20, ptIn: f32.Point{X: 0, Y: -20}, ptOut: f32.Point{X: 0, Y: -20}}, - {l: +20, ptIn: f32.Point{X: 0, Y: -30}, ptOut: f32.Point{X: 0, Y: -20}}, - - // l<0 & Y - {l: -20, ptIn: f32.Point{X: 0, Y: +30}, ptOut: f32.Point{X: 0, Y: -20}}, - {l: -20, ptIn: f32.Point{X: 0, Y: +20}, ptOut: f32.Point{X: 0, Y: -20}}, - {l: -20, ptIn: f32.Point{X: 0, Y: +10}, ptOut: f32.Point{X: 0, Y: -20}}, - {l: -20, ptIn: f32.Point{X: 0, Y: -10}, ptOut: f32.Point{X: 0, Y: +20}}, - {l: -20, ptIn: f32.Point{X: 0, Y: -20}, ptOut: f32.Point{X: 0, Y: +20}}, - {l: -20, ptIn: f32.Point{X: 0, Y: -30}, ptOut: f32.Point{X: 0, Y: +20}}, - - // l>0 && X=Y - {l: +20, ptIn: f32.Point{X: +90, Y: +90}, ptOut: f32.Point{X: +14.142137, Y: +14.142137}}, - {l: +20, ptIn: f32.Point{X: +30, Y: +30}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}}, - {l: +20, ptIn: f32.Point{X: +20, Y: +20}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}}, - {l: +20, ptIn: f32.Point{X: +10, Y: +10}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}}, - {l: +20, ptIn: f32.Point{X: -10, Y: -10}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}}, - {l: +20, ptIn: f32.Point{X: -20, Y: -20}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}}, - {l: +20, ptIn: f32.Point{X: -30, Y: -30}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}}, - {l: +20, ptIn: f32.Point{X: -90, Y: -90}, ptOut: f32.Point{X: -14.142137, Y: -14.142137}}, - - // l>0 && X=-Y - {l: +20, ptIn: f32.Point{X: +90, Y: -90}, ptOut: f32.Point{X: +14.142137, Y: -14.142137}}, - {l: +20, ptIn: f32.Point{X: +30, Y: -30}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}}, - {l: +20, ptIn: f32.Point{X: +20, Y: -20}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}}, - {l: +20, ptIn: f32.Point{X: +10, Y: -10}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}}, - {l: +20, ptIn: f32.Point{X: -10, Y: +10}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}}, - {l: +20, ptIn: f32.Point{X: -20, Y: +20}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}}, - {l: +20, ptIn: f32.Point{X: -30, Y: +30}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}}, - {l: +20, ptIn: f32.Point{X: -90, Y: +90}, ptOut: f32.Point{X: -14.142137, Y: +14.142137}}, - - // l<0 && X=Y - {l: -20, ptIn: f32.Point{X: +90, Y: +90}, ptOut: f32.Point{X: -14.142137, Y: -14.142137}}, - {l: -20, ptIn: f32.Point{X: +30, Y: +30}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}}, - {l: -20, ptIn: f32.Point{X: +20, Y: +20}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}}, - {l: -20, ptIn: f32.Point{X: +10, Y: +10}, ptOut: f32.Point{X: -14.142136, Y: -14.142136}}, - {l: -20, ptIn: f32.Point{X: -10, Y: -10}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}}, - {l: -20, ptIn: f32.Point{X: -20, Y: -20}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}}, - {l: -20, ptIn: f32.Point{X: -30, Y: -30}, ptOut: f32.Point{X: +14.142136, Y: +14.142136}}, - {l: -20, ptIn: f32.Point{X: -90, Y: -90}, ptOut: f32.Point{X: +14.142137, Y: +14.142137}}, - - // l<0 && X=-Y - {l: -20, ptIn: f32.Point{X: +90, Y: -90}, ptOut: f32.Point{X: -14.142137, Y: +14.142137}}, - {l: -20, ptIn: f32.Point{X: +30, Y: -30}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}}, - {l: -20, ptIn: f32.Point{X: +20, Y: -20}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}}, - {l: -20, ptIn: f32.Point{X: +10, Y: -10}, ptOut: f32.Point{X: -14.142136, Y: +14.142136}}, - {l: -20, ptIn: f32.Point{X: -10, Y: +10}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}}, - {l: -20, ptIn: f32.Point{X: -20, Y: +20}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}}, - {l: -20, ptIn: f32.Point{X: -30, Y: +30}, ptOut: f32.Point{X: +14.142136, Y: -14.142136}}, - {l: -20, ptIn: f32.Point{X: -90, Y: +90}, ptOut: f32.Point{X: +14.142137, Y: -14.142137}}, - - // l!=0 && Hypot=l - {l: 5, ptIn: f32.Point{X: 3, Y: 4}, ptOut: f32.Point{X: 3, Y: 4}}, - {l: 5, ptIn: f32.Point{X: 3, Y: -4}, ptOut: f32.Point{X: 3, Y: -4}}, - {l: 5, ptIn: f32.Point{X: -3, Y: -4}, ptOut: f32.Point{X: -3, Y: -4}}, - {l: 5, ptIn: f32.Point{X: -3, Y: 4}, ptOut: f32.Point{X: -3, Y: 4}}, - {l: -5, ptIn: f32.Point{X: 3, Y: 4}, ptOut: f32.Point{X: -3, Y: -4}}, - {l: -5, ptIn: f32.Point{X: 3, Y: -4}, ptOut: f32.Point{X: -3, Y: 4}}, - {l: -5, ptIn: f32.Point{X: -3, Y: -4}, ptOut: f32.Point{X: 3, Y: 4}}, - {l: -5, ptIn: f32.Point{X: -3, Y: 4}, ptOut: f32.Point{X: 3, Y: -4}}, - } - - for i, s := range scenarios { - t.Run(strconv.Itoa(i), func(t *testing.T) { - actual := normPt(s.ptIn, s.l) - if actual != s.ptOut { - t.Errorf("in: %v*%v, expected: %v, actual: %v", s.l, s.ptIn, s.ptOut, actual) - } - }) - - } -} - -func BenchmarkSplitCubic(b *testing.B) { - type scenario struct { - segments int - from, ctrl0, ctrl1, to f32.Point - } - - scenarios := []scenario{ - { - segments: 4, - from: f32.Pt(0, 0), - ctrl0: f32.Pt(10, 10), - ctrl1: f32.Pt(10, 10), - to: f32.Pt(20, 0), - }, - { - segments: 8, - from: f32.Pt(-145.90305, 703.21277), - ctrl0: f32.Pt(-940.20215, 606.05994), - ctrl1: f32.Pt(74.58341, 405.815), - to: f32.Pt(104.35474, -241.543), - }, - { - segments: 16, - from: f32.Pt(770.35626, 639.77765), - ctrl0: f32.Pt(735.57135, 545.07837), - ctrl1: f32.Pt(286.7138, 853.7052), - to: f32.Pt(286.7138, 890.5413), - }, - { - segments: 33, - from: f32.Pt(0, 0), - ctrl0: f32.Pt(0, 0), - ctrl1: f32.Pt(100, 100), - to: f32.Pt(100, 100), - }, - } - - for _, s := range scenarios { - s := s - b.Run(strconv.Itoa(s.segments), func(b *testing.B) { - from, ctrl0, ctrl1, to := s.from, s.ctrl0, s.ctrl1, s.to - quads := make([]QuadSegment, s.segments) - b.ResetTimer() - for b.Loop() { - quads = SplitCubic(from, ctrl0, ctrl1, to, quads[:0]) - } - if len(quads) != s.segments { - // this is just for checking that we are benchmarking similar splits - // when splitting algorithm splits differently, then it's fine to adjust the - // parameters to give appropriate number of segments. - b.Fatalf("expected %d but got %d", s.segments, len(quads)) - } - }) - } -} diff --git a/gio/internal/vk/vulkan.go b/gio/internal/vk/vulkan.go deleted file mode 100644 index 343516d..0000000 --- a/gio/internal/vk/vulkan.go +++ /dev/null @@ -1,2145 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build linux || freebsd -// +build linux freebsd - -package vk - -/* -#cgo linux freebsd LDFLAGS: -ldl -#cgo freebsd CFLAGS: -I/usr/local/include -#cgo CFLAGS: -Werror -Werror=return-type - -#define VK_NO_PROTOTYPES 1 -#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; -#include -#define __USE_GNU -#include -#include - -static VkResult vkCreateInstance(PFN_vkCreateInstance f, VkInstanceCreateInfo pCreateInfo, const VkAllocationCallbacks *pAllocator, VkInstance *pInstance) { - return f(&pCreateInfo, pAllocator, pInstance); -} - -static void vkDestroyInstance(PFN_vkDestroyInstance f, VkInstance instance, const VkAllocationCallbacks *pAllocator) { - f(instance, pAllocator); -} - -static VkResult vkEnumeratePhysicalDevices(PFN_vkEnumeratePhysicalDevices f, VkInstance instance, uint32_t *pPhysicalDeviceCount, VkPhysicalDevice *pPhysicalDevices) { - return f(instance, pPhysicalDeviceCount, pPhysicalDevices); -} - -static void vkGetPhysicalDeviceQueueFamilyProperties(PFN_vkGetPhysicalDeviceQueueFamilyProperties f, VkPhysicalDevice physicalDevice, uint32_t *pQueueFamilyPropertyCount, VkQueueFamilyProperties *pQueueFamilyProperties) { - f(physicalDevice, pQueueFamilyPropertyCount, pQueueFamilyProperties); -} - -static void vkGetPhysicalDeviceFormatProperties(PFN_vkGetPhysicalDeviceFormatProperties f, VkPhysicalDevice physicalDevice, VkFormat format, VkFormatProperties *pFormatProperties) { - f(physicalDevice, format, pFormatProperties); -} - -static VkResult vkCreateDevice(PFN_vkCreateDevice f, VkPhysicalDevice physicalDevice, VkDeviceCreateInfo pCreateInfo, VkDeviceQueueCreateInfo qinf, const VkAllocationCallbacks *pAllocator, VkDevice *pDevice) { - pCreateInfo.pQueueCreateInfos = &qinf; - return f(physicalDevice, &pCreateInfo, pAllocator, pDevice); -} - -static void vkDestroyDevice(PFN_vkDestroyDevice f, VkDevice device, const VkAllocationCallbacks *pAllocator) { - f(device, pAllocator); -} - -static void vkGetDeviceQueue(PFN_vkGetDeviceQueue f, VkDevice device, uint32_t queueFamilyIndex, uint32_t queueIndex, VkQueue *pQueue) { - f(device, queueFamilyIndex, queueIndex, pQueue); -} - -static VkResult vkCreateImageView(PFN_vkCreateImageView f, VkDevice device, const VkImageViewCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkImageView *pView) { - return f(device, pCreateInfo, pAllocator, pView); -} - -static void vkDestroyImageView(PFN_vkDestroyImageView f, VkDevice device, VkImageView imageView, const VkAllocationCallbacks *pAllocator) { - f(device, imageView, pAllocator); -} - -static VkResult vkCreateFramebuffer(PFN_vkCreateFramebuffer f, VkDevice device, VkFramebufferCreateInfo pCreateInfo, const VkAllocationCallbacks *pAllocator, VkFramebuffer *pFramebuffer) { - return f(device, &pCreateInfo, pAllocator, pFramebuffer); -} - -static void vkDestroyFramebuffer(PFN_vkDestroyFramebuffer f, VkDevice device, VkFramebuffer framebuffer, const VkAllocationCallbacks *pAllocator) { - f(device, framebuffer, pAllocator); -} - -static VkResult vkDeviceWaitIdle(PFN_vkDeviceWaitIdle f, VkDevice device) { - return f(device); -} - -static VkResult vkQueueWaitIdle(PFN_vkQueueWaitIdle f, VkQueue queue) { - return f(queue); -} - -static VkResult vkCreateSemaphore(PFN_vkCreateSemaphore f, VkDevice device, const VkSemaphoreCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSemaphore *pSemaphore) { - return f(device, pCreateInfo, pAllocator, pSemaphore); -} - -static void vkDestroySemaphore(PFN_vkDestroySemaphore f, VkDevice device, VkSemaphore semaphore, const VkAllocationCallbacks *pAllocator) { - f(device, semaphore, pAllocator); -} - -static VkResult vkCreateRenderPass(PFN_vkCreateRenderPass f, VkDevice device, VkRenderPassCreateInfo pCreateInfo, VkSubpassDescription subpassInf, const VkAllocationCallbacks *pAllocator, VkRenderPass *pRenderPass) { - pCreateInfo.pSubpasses = &subpassInf; - return f(device, &pCreateInfo, pAllocator, pRenderPass); -} - -static void vkDestroyRenderPass(PFN_vkDestroyRenderPass f, VkDevice device, VkRenderPass renderPass, const VkAllocationCallbacks *pAllocator) { - f(device, renderPass, pAllocator); -} - -static VkResult vkCreateCommandPool(PFN_vkCreateCommandPool f, VkDevice device, const VkCommandPoolCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkCommandPool *pCommandPool) { - return f(device, pCreateInfo, pAllocator, pCommandPool); -} - -static void vkDestroyCommandPool(PFN_vkDestroyCommandPool f, VkDevice device, VkCommandPool commandPool, const VkAllocationCallbacks *pAllocator) { - f(device, commandPool, pAllocator); -} - -static VkResult vkAllocateCommandBuffers(PFN_vkAllocateCommandBuffers f, VkDevice device, const VkCommandBufferAllocateInfo *pAllocateInfo, VkCommandBuffer *pCommandBuffers) { - return f(device, pAllocateInfo, pCommandBuffers); -} - -static void vkFreeCommandBuffers(PFN_vkFreeCommandBuffers f, VkDevice device, VkCommandPool commandPool, uint32_t commandBufferCount, const VkCommandBuffer *pCommandBuffers) { - f(device, commandPool, commandBufferCount, pCommandBuffers); -} - -static VkResult vkBeginCommandBuffer(PFN_vkBeginCommandBuffer f, VkCommandBuffer commandBuffer, VkCommandBufferBeginInfo pBeginInfo) { - return f(commandBuffer, &pBeginInfo); -} - -static VkResult vkEndCommandBuffer(PFN_vkEndCommandBuffer f, VkCommandBuffer commandBuffer) { - return f(commandBuffer); -} - -static VkResult vkQueueSubmit(PFN_vkQueueSubmit f, VkQueue queue, VkSubmitInfo pSubmits, VkFence fence) { - return f(queue, 1, &pSubmits, fence); -} - -static void vkCmdBeginRenderPass(PFN_vkCmdBeginRenderPass f, VkCommandBuffer commandBuffer, VkRenderPassBeginInfo pRenderPassBegin, VkSubpassContents contents) { - f(commandBuffer, &pRenderPassBegin, contents); -} - -static void vkCmdEndRenderPass(PFN_vkCmdEndRenderPass f, VkCommandBuffer commandBuffer) { - f(commandBuffer); -} - -static void vkCmdCopyBuffer(PFN_vkCmdCopyBuffer f, VkCommandBuffer commandBuffer, VkBuffer srcBuffer, VkBuffer dstBuffer, uint32_t regionCount, const VkBufferCopy *pRegions) { - f(commandBuffer, srcBuffer, dstBuffer, regionCount, pRegions); -} - -static void vkCmdCopyBufferToImage(PFN_vkCmdCopyBufferToImage f, VkCommandBuffer commandBuffer, VkBuffer srcBuffer, VkImage dstImage, VkImageLayout dstImageLayout, uint32_t regionCount, const VkBufferImageCopy *pRegions) { - f(commandBuffer, srcBuffer, dstImage, dstImageLayout, regionCount, pRegions); -} - -static void vkCmdPipelineBarrier(PFN_vkCmdPipelineBarrier f, VkCommandBuffer commandBuffer, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask, VkDependencyFlags dependencyFlags, uint32_t memoryBarrierCount, const VkMemoryBarrier *pMemoryBarriers, uint32_t bufferMemoryBarrierCount, const VkBufferMemoryBarrier *pBufferMemoryBarriers, uint32_t imageMemoryBarrierCount, const VkImageMemoryBarrier *pImageMemoryBarriers) { - f(commandBuffer, srcStageMask, dstStageMask, dependencyFlags, memoryBarrierCount, pMemoryBarriers, bufferMemoryBarrierCount, pBufferMemoryBarriers, imageMemoryBarrierCount, pImageMemoryBarriers); -} - -static void vkCmdPushConstants(PFN_vkCmdPushConstants f, VkCommandBuffer commandBuffer, VkPipelineLayout layout, VkShaderStageFlags stageFlags, uint32_t offset, uint32_t size, const void *pValues) { - f(commandBuffer, layout, stageFlags, offset, size, pValues); -} - -static void vkCmdBindPipeline(PFN_vkCmdBindPipeline f, VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipeline pipeline) { - f(commandBuffer, pipelineBindPoint, pipeline); -} - -static void vkCmdBindVertexBuffers(PFN_vkCmdBindVertexBuffers f, VkCommandBuffer commandBuffer, uint32_t firstBinding, uint32_t bindingCount, const VkBuffer *pBuffers, const VkDeviceSize *pOffsets) { - f(commandBuffer, firstBinding, bindingCount, pBuffers, pOffsets); -} - -static void vkCmdSetViewport(PFN_vkCmdSetViewport f, VkCommandBuffer commandBuffer, uint32_t firstViewport, uint32_t viewportCount, const VkViewport *pViewports) { - f(commandBuffer, firstViewport, viewportCount, pViewports); -} - -static void vkCmdBindIndexBuffer(PFN_vkCmdBindIndexBuffer f, VkCommandBuffer commandBuffer, VkBuffer buffer, VkDeviceSize offset, VkIndexType indexType) { - f(commandBuffer, buffer, offset, indexType); -} - -static void vkCmdDraw(PFN_vkCmdDraw f, VkCommandBuffer commandBuffer, uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance) { - f(commandBuffer, vertexCount, instanceCount, firstVertex, firstInstance); -} - -static void vkCmdDrawIndexed(PFN_vkCmdDrawIndexed f, VkCommandBuffer commandBuffer, uint32_t indexCount, uint32_t instanceCount, uint32_t firstIndex, int32_t vertexOffset, uint32_t firstInstance) { - f(commandBuffer, indexCount, instanceCount, firstIndex, vertexOffset, firstInstance); -} - -static void vkCmdBindDescriptorSets(PFN_vkCmdBindDescriptorSets f, VkCommandBuffer commandBuffer, VkPipelineBindPoint pipelineBindPoint, VkPipelineLayout layout, uint32_t firstSet, uint32_t descriptorSetCount, const VkDescriptorSet *pDescriptorSets, uint32_t dynamicOffsetCount, const uint32_t *pDynamicOffsets) { - f(commandBuffer, pipelineBindPoint, layout, firstSet, descriptorSetCount, pDescriptorSets, dynamicOffsetCount, pDynamicOffsets); -} - -static void vkCmdCopyImageToBuffer(PFN_vkCmdCopyImageToBuffer f, VkCommandBuffer commandBuffer, VkImage srcImage, VkImageLayout srcImageLayout, VkBuffer dstBuffer, uint32_t regionCount, const VkBufferImageCopy *pRegions) { - f(commandBuffer, srcImage, srcImageLayout, dstBuffer, regionCount, pRegions); -} - -static void vkCmdDispatch(PFN_vkCmdDispatch f, VkCommandBuffer commandBuffer, uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) { - f(commandBuffer, groupCountX, groupCountY, groupCountZ); -} - -static VkResult vkCreateImage(PFN_vkCreateImage f, VkDevice device, const VkImageCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkImage *pImage) { - return f(device, pCreateInfo, pAllocator, pImage); -} - -static void vkDestroyImage(PFN_vkDestroyImage f, VkDevice device, VkImage image, const VkAllocationCallbacks *pAllocator) { - f(device, image, pAllocator); -} - -static void vkGetImageMemoryRequirements(PFN_vkGetImageMemoryRequirements f, VkDevice device, VkImage image, VkMemoryRequirements *pMemoryRequirements) { - f(device, image, pMemoryRequirements); -} - -static VkResult vkAllocateMemory(PFN_vkAllocateMemory f, VkDevice device, const VkMemoryAllocateInfo *pAllocateInfo, const VkAllocationCallbacks *pAllocator, VkDeviceMemory *pMemory) { - return f(device, pAllocateInfo, pAllocator, pMemory); -} - -static VkResult vkBindImageMemory(PFN_vkBindImageMemory f, VkDevice device, VkImage image, VkDeviceMemory memory, VkDeviceSize memoryOffset) { - return f(device, image, memory, memoryOffset); -} - -static void vkFreeMemory(PFN_vkFreeMemory f, VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks *pAllocator) { - f(device, memory, pAllocator); -} - -static void vkGetPhysicalDeviceMemoryProperties(PFN_vkGetPhysicalDeviceMemoryProperties f, VkPhysicalDevice physicalDevice, VkPhysicalDeviceMemoryProperties *pMemoryProperties) { - f(physicalDevice, pMemoryProperties); -} - -static VkResult vkCreateSampler(PFN_vkCreateSampler f,VkDevice device, const VkSamplerCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSampler *pSampler) { - return f(device, pCreateInfo, pAllocator, pSampler); -} - -static void vkDestroySampler(PFN_vkDestroySampler f, VkDevice device, VkSampler sampler, const VkAllocationCallbacks *pAllocator) { - f(device, sampler, pAllocator); -} - -static VkResult vkCreateBuffer(PFN_vkCreateBuffer f, VkDevice device, const VkBufferCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkBuffer *pBuffer) { - return f(device, pCreateInfo, pAllocator, pBuffer); -} - -static void vkDestroyBuffer(PFN_vkDestroyBuffer f, VkDevice device, VkBuffer buffer, const VkAllocationCallbacks *pAllocator) { - f(device, buffer, pAllocator); -} - -static void vkGetBufferMemoryRequirements(PFN_vkGetBufferMemoryRequirements f, VkDevice device, VkBuffer buffer, VkMemoryRequirements *pMemoryRequirements) { - f(device, buffer, pMemoryRequirements); -} - -static VkResult vkBindBufferMemory(PFN_vkBindBufferMemory f, VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize memoryOffset) { - return f(device, buffer, memory, memoryOffset); -} - -static VkResult vkCreateShaderModule(PFN_vkCreateShaderModule f, VkDevice device, VkShaderModuleCreateInfo pCreateInfo, const VkAllocationCallbacks *pAllocator, VkShaderModule *pShaderModule) { - return f(device, &pCreateInfo, pAllocator, pShaderModule); -} - -static void vkDestroyShaderModule(PFN_vkDestroyShaderModule f, VkDevice device, VkShaderModule shaderModule, const VkAllocationCallbacks *pAllocator) { - f(device, shaderModule, pAllocator); -} - -static VkResult vkCreateGraphicsPipelines(PFN_vkCreateGraphicsPipelines f, VkDevice device, VkPipelineCache pipelineCache, VkGraphicsPipelineCreateInfo pCreateInfo, VkPipelineDynamicStateCreateInfo dynInf, VkPipelineColorBlendStateCreateInfo blendInf, VkPipelineVertexInputStateCreateInfo vertexInf, VkPipelineViewportStateCreateInfo viewportInf, const VkAllocationCallbacks *pAllocator, VkPipeline *pPipelines) { - pCreateInfo.pDynamicState = &dynInf; - pCreateInfo.pViewportState = &viewportInf; - pCreateInfo.pColorBlendState = &blendInf; - pCreateInfo.pVertexInputState = &vertexInf; - return f(device, pipelineCache, 1, &pCreateInfo, pAllocator, pPipelines); -} - -static void vkDestroyPipeline(PFN_vkDestroyPipeline f, VkDevice device, VkPipeline pipeline, const VkAllocationCallbacks *pAllocator) { - f(device, pipeline, pAllocator); -} - -static VkResult vkCreatePipelineLayout(PFN_vkCreatePipelineLayout f, VkDevice device, VkPipelineLayoutCreateInfo pCreateInfo, const VkAllocationCallbacks *pAllocator, VkPipelineLayout *pPipelineLayout) { - return f(device, &pCreateInfo, pAllocator, pPipelineLayout); -} - -static void vkDestroyPipelineLayout(PFN_vkDestroyPipelineLayout f, VkDevice device, VkPipelineLayout pipelineLayout, const VkAllocationCallbacks *pAllocator) { - f(device, pipelineLayout, pAllocator); -} - -static VkResult vkCreateDescriptorSetLayout(PFN_vkCreateDescriptorSetLayout f, VkDevice device, VkDescriptorSetLayoutCreateInfo pCreateInfo, const VkAllocationCallbacks *pAllocator, VkDescriptorSetLayout *pSetLayout) { - return f(device, &pCreateInfo, pAllocator, pSetLayout); -} - -static void vkDestroyDescriptorSetLayout(PFN_vkDestroyDescriptorSetLayout f, VkDevice device, VkDescriptorSetLayout descriptorSetLayout, const VkAllocationCallbacks *pAllocator) { - f(device, descriptorSetLayout, pAllocator); -} - -static VkResult vkMapMemory(PFN_vkMapMemory f, VkDevice device, VkDeviceMemory memory, VkDeviceSize offset, VkDeviceSize size, VkMemoryMapFlags flags, void **ppData) { - return f(device, memory, offset, size, flags, ppData); -} - -static void vkUnmapMemory(PFN_vkUnmapMemory f, VkDevice device, VkDeviceMemory memory) { - f(device, memory); -} - -static VkResult vkResetCommandBuffer(PFN_vkResetCommandBuffer f, VkCommandBuffer commandBuffer, VkCommandBufferResetFlags flags) { - return f(commandBuffer, flags); -} - -static VkResult vkCreateDescriptorPool(PFN_vkCreateDescriptorPool f, VkDevice device, VkDescriptorPoolCreateInfo pCreateInfo, const VkAllocationCallbacks *pAllocator, VkDescriptorPool *pDescriptorPool) { - return f(device, &pCreateInfo, pAllocator, pDescriptorPool); -} - -static void vkDestroyDescriptorPool(PFN_vkDestroyDescriptorPool f, VkDevice device, VkDescriptorPool descriptorPool, const VkAllocationCallbacks *pAllocator) { - f(device, descriptorPool, pAllocator); -} - -static VkResult vkAllocateDescriptorSets(PFN_vkAllocateDescriptorSets f, VkDevice device, VkDescriptorSetAllocateInfo pAllocateInfo, VkDescriptorSet *pDescriptorSets) { - return f(device, &pAllocateInfo, pDescriptorSets); -} - -static VkResult vkFreeDescriptorSets(PFN_vkFreeDescriptorSets f, VkDevice device, VkDescriptorPool descriptorPool, uint32_t descriptorSetCount, const VkDescriptorSet *pDescriptorSets) { - return f(device, descriptorPool, descriptorSetCount, pDescriptorSets); -} - -static void vkUpdateDescriptorSets(PFN_vkUpdateDescriptorSets f, VkDevice device, VkWriteDescriptorSet pDescriptorWrite, uint32_t descriptorCopyCount, const VkCopyDescriptorSet *pDescriptorCopies) { - f(device, 1, &pDescriptorWrite, descriptorCopyCount, pDescriptorCopies); -} - -static VkResult vkResetDescriptorPool(PFN_vkResetDescriptorPool f, VkDevice device, VkDescriptorPool descriptorPool, VkDescriptorPoolResetFlags flags) { - return f(device, descriptorPool, flags); -} - -static void vkCmdBlitImage(PFN_vkCmdBlitImage f, VkCommandBuffer commandBuffer, VkImage srcImage, VkImageLayout srcImageLayout, VkImage dstImage, VkImageLayout dstImageLayout, uint32_t regionCount, const VkImageBlit* pRegions, VkFilter filter) { - f(commandBuffer, srcImage, srcImageLayout, dstImage, dstImageLayout, regionCount, pRegions, filter); -} - -static void vkCmdCopyImage(PFN_vkCmdCopyImage f, VkCommandBuffer commandBuffer, VkImage srcImage, VkImageLayout srcImageLayout, VkImage dstImage, VkImageLayout dstImageLayout, uint32_t regionCount, const VkImageCopy *pRegions) { - f(commandBuffer, srcImage, srcImageLayout, dstImage, dstImageLayout, regionCount, pRegions); -} - -static VkResult vkCreateComputePipelines(PFN_vkCreateComputePipelines f, VkDevice device, VkPipelineCache pipelineCache, uint32_t createInfoCount, const VkComputePipelineCreateInfo *pCreateInfos, const VkAllocationCallbacks *pAllocator, VkPipeline *pPipelines) { - return f(device, pipelineCache, createInfoCount, pCreateInfos, pAllocator, pPipelines); -} - -static VkResult vkCreateFence(PFN_vkCreateFence f, VkDevice device, const VkFenceCreateInfo *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkFence *pFence) { - return f(device, pCreateInfo, pAllocator, pFence); -} - -static void vkDestroyFence(PFN_vkDestroyFence f, VkDevice device, VkFence fence, const VkAllocationCallbacks *pAllocator) { - f(device, fence, pAllocator); -} - -static VkResult vkWaitForFences(PFN_vkWaitForFences f, VkDevice device, uint32_t fenceCount, const VkFence *pFences, VkBool32 waitAll, uint64_t timeout) { - return f(device, fenceCount, pFences, waitAll, timeout); -} - -static VkResult vkResetFences(PFN_vkResetFences f, VkDevice device, uint32_t fenceCount, const VkFence *pFences) { - return f(device, fenceCount, pFences); -} - -static void vkGetPhysicalDeviceProperties(PFN_vkGetPhysicalDeviceProperties f, VkPhysicalDevice physicalDevice, VkPhysicalDeviceProperties *pProperties) { - f(physicalDevice, pProperties); -} - -static VkResult vkGetPhysicalDeviceSurfaceSupportKHR(PFN_vkGetPhysicalDeviceSurfaceSupportKHR f, VkPhysicalDevice physicalDevice, uint32_t queueFamilyIndex, VkSurfaceKHR surface, VkBool32 *pSupported) { - return f(physicalDevice, queueFamilyIndex, surface, pSupported); -} - -static void vkDestroySurfaceKHR(PFN_vkDestroySurfaceKHR f, VkInstance instance, VkSurfaceKHR surface, const VkAllocationCallbacks *pAllocator) { - f(instance, surface, pAllocator); -} - -static VkResult vkGetPhysicalDeviceSurfaceFormatsKHR(PFN_vkGetPhysicalDeviceSurfaceFormatsKHR f, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t *pSurfaceFormatCount, VkSurfaceFormatKHR *pSurfaceFormats) { - return f(physicalDevice, surface, pSurfaceFormatCount, pSurfaceFormats); -} - -static VkResult vkGetPhysicalDeviceSurfacePresentModesKHR(PFN_vkGetPhysicalDeviceSurfacePresentModesKHR f, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t *pPresentModeCount, VkPresentModeKHR *pPresentModes) { - return f(physicalDevice, surface, pPresentModeCount, pPresentModes); -} - -static VkResult vkGetPhysicalDeviceSurfaceCapabilitiesKHR(PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR f, VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, VkSurfaceCapabilitiesKHR *pSurfaceCapabilities) { - return f(physicalDevice, surface, pSurfaceCapabilities); -} - -static VkResult vkCreateSwapchainKHR(PFN_vkCreateSwapchainKHR f, VkDevice device, const VkSwapchainCreateInfoKHR *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSwapchainKHR *pSwapchain) { - return f(device, pCreateInfo, pAllocator, pSwapchain); -} - -static void vkDestroySwapchainKHR(PFN_vkDestroySwapchainKHR f, VkDevice device, VkSwapchainKHR swapchain, const VkAllocationCallbacks *pAllocator) { - f(device, swapchain, pAllocator); -} - -static VkResult vkGetSwapchainImagesKHR(PFN_vkGetSwapchainImagesKHR f, VkDevice device, VkSwapchainKHR swapchain, uint32_t *pSwapchainImageCount, VkImage *pSwapchainImages) { - return f(device, swapchain, pSwapchainImageCount, pSwapchainImages); -} - -// indexAndResult holds both an integer and a result returned by value, to -// avoid Go heap allocation of the integer with Vulkan's return style. -struct intAndResult { - uint32_t uint; - VkResult res; -}; - -static struct intAndResult vkAcquireNextImageKHR(PFN_vkAcquireNextImageKHR f, VkDevice device, VkSwapchainKHR swapchain, uint64_t timeout, VkSemaphore semaphore, VkFence fence) { - struct intAndResult res; - res.res = f(device, swapchain, timeout, semaphore, fence, &res.uint); - return res; -} - -static VkResult vkQueuePresentKHR(PFN_vkQueuePresentKHR f, VkQueue queue, const VkPresentInfoKHR pPresentInfo) { - return f(queue, &pPresentInfo); -} -*/ -import "C" - -import ( - "errors" - "fmt" - "image" - "math" - "reflect" - "runtime" - "sync" - "unsafe" -) - -type ( - AttachmentLoadOp = C.VkAttachmentLoadOp - AccessFlags = C.VkAccessFlags - BlendFactor = C.VkBlendFactor - Buffer = C.VkBuffer - BufferImageCopy = C.VkBufferImageCopy - BufferMemoryBarrier = C.VkBufferMemoryBarrier - BufferUsageFlags = C.VkBufferUsageFlags - CommandPool = C.VkCommandPool - CommandBuffer = C.VkCommandBuffer - DependencyFlags = C.VkDependencyFlags - DescriptorPool = C.VkDescriptorPool - DescriptorPoolSize = C.VkDescriptorPoolSize - DescriptorSet = C.VkDescriptorSet - DescriptorSetLayout = C.VkDescriptorSetLayout - DescriptorType = C.VkDescriptorType - Device = C.VkDevice - DeviceMemory = C.VkDeviceMemory - DeviceSize = C.VkDeviceSize - Fence = C.VkFence - Queue = C.VkQueue - IndexType = C.VkIndexType - Image = C.VkImage - ImageBlit = C.VkImageBlit - ImageCopy = C.VkImageCopy - ImageLayout = C.VkImageLayout - ImageMemoryBarrier = C.VkImageMemoryBarrier - ImageUsageFlags = C.VkImageUsageFlags - ImageView = C.VkImageView - Instance = C.VkInstance - Filter = C.VkFilter - Format = C.VkFormat - FormatFeatureFlags = C.VkFormatFeatureFlags - Framebuffer = C.VkFramebuffer - MemoryBarrier = C.VkMemoryBarrier - MemoryPropertyFlags = C.VkMemoryPropertyFlags - Pipeline = C.VkPipeline - PipelineBindPoint = C.VkPipelineBindPoint - PipelineLayout = C.VkPipelineLayout - PipelineStageFlags = C.VkPipelineStageFlags - PhysicalDevice = C.VkPhysicalDevice - PrimitiveTopology = C.VkPrimitiveTopology - PushConstantRange = C.VkPushConstantRange - QueueFamilyProperties = C.VkQueueFamilyProperties - QueueFlags = C.VkQueueFlags - RenderPass = C.VkRenderPass - Sampler = C.VkSampler - SamplerMipmapMode = C.VkSamplerMipmapMode - Semaphore = C.VkSemaphore - ShaderModule = C.VkShaderModule - ShaderStageFlags = C.VkShaderStageFlags - SubpassDependency = C.VkSubpassDependency - Viewport = C.VkViewport - WriteDescriptorSet = C.VkWriteDescriptorSet - - Surface = C.VkSurfaceKHR - SurfaceCapabilities = C.VkSurfaceCapabilitiesKHR - - Swapchain = C.VkSwapchainKHR -) - -type VertexInputBindingDescription struct { - Binding int - Stride int -} - -type VertexInputAttributeDescription struct { - Location int - Binding int - Format Format - Offset int -} - -type DescriptorSetLayoutBinding struct { - Binding int - DescriptorType DescriptorType - StageFlags ShaderStageFlags -} - -type Error C.VkResult - -const ( - FORMAT_R8G8B8A8_UNORM Format = C.VK_FORMAT_R8G8B8A8_UNORM - FORMAT_B8G8R8A8_SRGB Format = C.VK_FORMAT_B8G8R8A8_SRGB - FORMAT_R8G8B8A8_SRGB Format = C.VK_FORMAT_R8G8B8A8_SRGB - FORMAT_R16_SFLOAT Format = C.VK_FORMAT_R16_SFLOAT - FORMAT_R32_SFLOAT Format = C.VK_FORMAT_R32_SFLOAT - FORMAT_R32G32_SFLOAT Format = C.VK_FORMAT_R32G32_SFLOAT - FORMAT_R32G32B32_SFLOAT Format = C.VK_FORMAT_R32G32B32_SFLOAT - FORMAT_R32G32B32A32_SFLOAT Format = C.VK_FORMAT_R32G32B32A32_SFLOAT - - FORMAT_FEATURE_COLOR_ATTACHMENT_BIT FormatFeatureFlags = C.VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT - FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT FormatFeatureFlags = C.VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BLEND_BIT - FORMAT_FEATURE_SAMPLED_IMAGE_BIT FormatFeatureFlags = C.VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT - FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT FormatFeatureFlags = C.VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT - - IMAGE_USAGE_SAMPLED_BIT ImageUsageFlags = C.VK_IMAGE_USAGE_SAMPLED_BIT - IMAGE_USAGE_COLOR_ATTACHMENT_BIT ImageUsageFlags = C.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT - IMAGE_USAGE_STORAGE_BIT ImageUsageFlags = C.VK_IMAGE_USAGE_STORAGE_BIT - IMAGE_USAGE_TRANSFER_DST_BIT ImageUsageFlags = C.VK_IMAGE_USAGE_TRANSFER_DST_BIT - IMAGE_USAGE_TRANSFER_SRC_BIT ImageUsageFlags = C.VK_IMAGE_USAGE_TRANSFER_SRC_BIT - - FILTER_NEAREST Filter = C.VK_FILTER_NEAREST - FILTER_LINEAR Filter = C.VK_FILTER_LINEAR - - ATTACHMENT_LOAD_OP_CLEAR AttachmentLoadOp = C.VK_ATTACHMENT_LOAD_OP_CLEAR - ATTACHMENT_LOAD_OP_DONT_CARE AttachmentLoadOp = C.VK_ATTACHMENT_LOAD_OP_DONT_CARE - ATTACHMENT_LOAD_OP_LOAD AttachmentLoadOp = C.VK_ATTACHMENT_LOAD_OP_LOAD - - IMAGE_LAYOUT_UNDEFINED ImageLayout = C.VK_IMAGE_LAYOUT_UNDEFINED - IMAGE_LAYOUT_PRESENT_SRC_KHR ImageLayout = C.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR - IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ImageLayout = C.VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL - IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL ImageLayout = C.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL - IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ImageLayout = C.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL - IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL ImageLayout = C.VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL - IMAGE_LAYOUT_GENERAL ImageLayout = C.VK_IMAGE_LAYOUT_GENERAL - - BUFFER_USAGE_TRANSFER_DST_BIT BufferUsageFlags = C.VK_BUFFER_USAGE_TRANSFER_DST_BIT - BUFFER_USAGE_TRANSFER_SRC_BIT BufferUsageFlags = C.VK_BUFFER_USAGE_TRANSFER_SRC_BIT - BUFFER_USAGE_UNIFORM_BUFFER_BIT BufferUsageFlags = C.VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT - BUFFER_USAGE_STORAGE_BUFFER_BIT BufferUsageFlags = C.VK_BUFFER_USAGE_STORAGE_BUFFER_BIT - BUFFER_USAGE_INDEX_BUFFER_BIT BufferUsageFlags = C.VK_BUFFER_USAGE_INDEX_BUFFER_BIT - BUFFER_USAGE_VERTEX_BUFFER_BIT BufferUsageFlags = C.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT - - ERROR_OUT_OF_DATE_KHR = Error(C.VK_ERROR_OUT_OF_DATE_KHR) - ERROR_SURFACE_LOST_KHR = Error(C.VK_ERROR_SURFACE_LOST_KHR) - ERROR_DEVICE_LOST = Error(C.VK_ERROR_DEVICE_LOST) - SUBOPTIMAL_KHR = Error(C.VK_SUBOPTIMAL_KHR) - - FENCE_CREATE_SIGNALED_BIT = 0x00000001 - - BLEND_FACTOR_ZERO BlendFactor = C.VK_BLEND_FACTOR_ZERO - BLEND_FACTOR_ONE BlendFactor = C.VK_BLEND_FACTOR_ONE - BLEND_FACTOR_ONE_MINUS_SRC_ALPHA BlendFactor = C.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA - BLEND_FACTOR_DST_COLOR BlendFactor = C.VK_BLEND_FACTOR_DST_COLOR - - PRIMITIVE_TOPOLOGY_TRIANGLE_LIST PrimitiveTopology = C.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST - PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP PrimitiveTopology = C.VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP - - SHADER_STAGE_VERTEX_BIT ShaderStageFlags = C.VK_SHADER_STAGE_VERTEX_BIT - SHADER_STAGE_FRAGMENT_BIT ShaderStageFlags = C.VK_SHADER_STAGE_FRAGMENT_BIT - SHADER_STAGE_COMPUTE_BIT ShaderStageFlags = C.VK_SHADER_STAGE_COMPUTE_BIT - - DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER DescriptorType = C.VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER - DESCRIPTOR_TYPE_UNIFORM_BUFFER DescriptorType = C.VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER - DESCRIPTOR_TYPE_STORAGE_BUFFER DescriptorType = C.VK_DESCRIPTOR_TYPE_STORAGE_BUFFER - DESCRIPTOR_TYPE_STORAGE_IMAGE DescriptorType = C.VK_DESCRIPTOR_TYPE_STORAGE_IMAGE - - MEMORY_PROPERTY_DEVICE_LOCAL_BIT MemoryPropertyFlags = C.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT - MEMORY_PROPERTY_HOST_VISIBLE_BIT MemoryPropertyFlags = C.VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT - MEMORY_PROPERTY_HOST_COHERENT_BIT MemoryPropertyFlags = C.VK_MEMORY_PROPERTY_HOST_COHERENT_BIT - - DEPENDENCY_BY_REGION_BIT DependencyFlags = C.VK_DEPENDENCY_BY_REGION_BIT - - PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT PipelineStageFlags = C.VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT - PIPELINE_STAGE_TRANSFER_BIT PipelineStageFlags = C.VK_PIPELINE_STAGE_TRANSFER_BIT - PIPELINE_STAGE_FRAGMENT_SHADER_BIT PipelineStageFlags = C.VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT - PIPELINE_STAGE_COMPUTE_SHADER_BIT PipelineStageFlags = C.VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT - PIPELINE_STAGE_TOP_OF_PIPE_BIT PipelineStageFlags = C.VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT - PIPELINE_STAGE_HOST_BIT PipelineStageFlags = C.VK_PIPELINE_STAGE_HOST_BIT - PIPELINE_STAGE_VERTEX_INPUT_BIT PipelineStageFlags = C.VK_PIPELINE_STAGE_VERTEX_INPUT_BIT - PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT PipelineStageFlags = C.VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT - - ACCESS_MEMORY_READ_BIT AccessFlags = C.VK_ACCESS_MEMORY_READ_BIT - ACCESS_MEMORY_WRITE_BIT AccessFlags = C.VK_ACCESS_MEMORY_WRITE_BIT - ACCESS_TRANSFER_READ_BIT AccessFlags = C.VK_ACCESS_TRANSFER_READ_BIT - ACCESS_TRANSFER_WRITE_BIT AccessFlags = C.VK_ACCESS_TRANSFER_WRITE_BIT - ACCESS_SHADER_READ_BIT AccessFlags = C.VK_ACCESS_SHADER_READ_BIT - ACCESS_SHADER_WRITE_BIT AccessFlags = C.VK_ACCESS_SHADER_WRITE_BIT - ACCESS_COLOR_ATTACHMENT_READ_BIT AccessFlags = C.VK_ACCESS_COLOR_ATTACHMENT_READ_BIT - ACCESS_COLOR_ATTACHMENT_WRITE_BIT AccessFlags = C.VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT - ACCESS_HOST_READ_BIT AccessFlags = C.VK_ACCESS_HOST_READ_BIT - ACCESS_HOST_WRITE_BIT AccessFlags = C.VK_ACCESS_HOST_WRITE_BIT - ACCESS_VERTEX_ATTRIBUTE_READ_BIT AccessFlags = C.VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT - ACCESS_INDEX_READ_BIT AccessFlags = C.VK_ACCESS_INDEX_READ_BIT - - PIPELINE_BIND_POINT_COMPUTE PipelineBindPoint = C.VK_PIPELINE_BIND_POINT_COMPUTE - PIPELINE_BIND_POINT_GRAPHICS PipelineBindPoint = C.VK_PIPELINE_BIND_POINT_GRAPHICS - - INDEX_TYPE_UINT16 IndexType = C.VK_INDEX_TYPE_UINT16 - INDEX_TYPE_UINT32 IndexType = C.VK_INDEX_TYPE_UINT32 - - QUEUE_GRAPHICS_BIT QueueFlags = C.VK_QUEUE_GRAPHICS_BIT - QUEUE_COMPUTE_BIT QueueFlags = C.VK_QUEUE_COMPUTE_BIT - - SAMPLER_MIPMAP_MODE_NEAREST SamplerMipmapMode = C.VK_SAMPLER_MIPMAP_MODE_NEAREST - SAMPLER_MIPMAP_MODE_LINEAR SamplerMipmapMode = C.VK_SAMPLER_MIPMAP_MODE_LINEAR - - REMAINING_MIP_LEVELS = -1 -) - -var ( - once sync.Once - loadErr error - - loadFuncs []func(dlopen func(name string) *[0]byte) -) - -var funcs struct { - vkCreateInstance C.PFN_vkCreateInstance - vkDestroyInstance C.PFN_vkDestroyInstance - vkEnumeratePhysicalDevices C.PFN_vkEnumeratePhysicalDevices - vkGetPhysicalDeviceQueueFamilyProperties C.PFN_vkGetPhysicalDeviceQueueFamilyProperties - vkGetPhysicalDeviceFormatProperties C.PFN_vkGetPhysicalDeviceFormatProperties - vkCreateDevice C.PFN_vkCreateDevice - vkDestroyDevice C.PFN_vkDestroyDevice - vkGetDeviceQueue C.PFN_vkGetDeviceQueue - vkCreateImageView C.PFN_vkCreateImageView - vkDestroyImageView C.PFN_vkDestroyImageView - vkCreateFramebuffer C.PFN_vkCreateFramebuffer - vkDestroyFramebuffer C.PFN_vkDestroyFramebuffer - vkDeviceWaitIdle C.PFN_vkDeviceWaitIdle - vkQueueWaitIdle C.PFN_vkQueueWaitIdle - vkCreateSemaphore C.PFN_vkCreateSemaphore - vkDestroySemaphore C.PFN_vkDestroySemaphore - vkCreateRenderPass C.PFN_vkCreateRenderPass - vkDestroyRenderPass C.PFN_vkDestroyRenderPass - vkCreateCommandPool C.PFN_vkCreateCommandPool - vkDestroyCommandPool C.PFN_vkDestroyCommandPool - vkAllocateCommandBuffers C.PFN_vkAllocateCommandBuffers - vkFreeCommandBuffers C.PFN_vkFreeCommandBuffers - vkBeginCommandBuffer C.PFN_vkBeginCommandBuffer - vkEndCommandBuffer C.PFN_vkEndCommandBuffer - vkQueueSubmit C.PFN_vkQueueSubmit - vkCmdBeginRenderPass C.PFN_vkCmdBeginRenderPass - vkCmdEndRenderPass C.PFN_vkCmdEndRenderPass - vkCmdCopyBuffer C.PFN_vkCmdCopyBuffer - vkCmdCopyBufferToImage C.PFN_vkCmdCopyBufferToImage - vkCmdPipelineBarrier C.PFN_vkCmdPipelineBarrier - vkCmdPushConstants C.PFN_vkCmdPushConstants - vkCmdBindPipeline C.PFN_vkCmdBindPipeline - vkCmdBindVertexBuffers C.PFN_vkCmdBindVertexBuffers - vkCmdSetViewport C.PFN_vkCmdSetViewport - vkCmdBindIndexBuffer C.PFN_vkCmdBindIndexBuffer - vkCmdDraw C.PFN_vkCmdDraw - vkCmdDrawIndexed C.PFN_vkCmdDrawIndexed - vkCmdBindDescriptorSets C.PFN_vkCmdBindDescriptorSets - vkCmdCopyImageToBuffer C.PFN_vkCmdCopyImageToBuffer - vkCmdDispatch C.PFN_vkCmdDispatch - vkCreateImage C.PFN_vkCreateImage - vkDestroyImage C.PFN_vkDestroyImage - vkGetImageMemoryRequirements C.PFN_vkGetImageMemoryRequirements - vkAllocateMemory C.PFN_vkAllocateMemory - vkBindImageMemory C.PFN_vkBindImageMemory - vkFreeMemory C.PFN_vkFreeMemory - vkGetPhysicalDeviceMemoryProperties C.PFN_vkGetPhysicalDeviceMemoryProperties - vkCreateSampler C.PFN_vkCreateSampler - vkDestroySampler C.PFN_vkDestroySampler - vkCreateBuffer C.PFN_vkCreateBuffer - vkDestroyBuffer C.PFN_vkDestroyBuffer - vkGetBufferMemoryRequirements C.PFN_vkGetBufferMemoryRequirements - vkBindBufferMemory C.PFN_vkBindBufferMemory - vkCreateShaderModule C.PFN_vkCreateShaderModule - vkDestroyShaderModule C.PFN_vkDestroyShaderModule - vkCreateGraphicsPipelines C.PFN_vkCreateGraphicsPipelines - vkDestroyPipeline C.PFN_vkDestroyPipeline - vkCreatePipelineLayout C.PFN_vkCreatePipelineLayout - vkDestroyPipelineLayout C.PFN_vkDestroyPipelineLayout - vkCreateDescriptorSetLayout C.PFN_vkCreateDescriptorSetLayout - vkDestroyDescriptorSetLayout C.PFN_vkDestroyDescriptorSetLayout - vkMapMemory C.PFN_vkMapMemory - vkUnmapMemory C.PFN_vkUnmapMemory - vkResetCommandBuffer C.PFN_vkResetCommandBuffer - vkCreateDescriptorPool C.PFN_vkCreateDescriptorPool - vkDestroyDescriptorPool C.PFN_vkDestroyDescriptorPool - vkAllocateDescriptorSets C.PFN_vkAllocateDescriptorSets - vkFreeDescriptorSets C.PFN_vkFreeDescriptorSets - vkUpdateDescriptorSets C.PFN_vkUpdateDescriptorSets - vkResetDescriptorPool C.PFN_vkResetDescriptorPool - vkCmdBlitImage C.PFN_vkCmdBlitImage - vkCmdCopyImage C.PFN_vkCmdCopyImage - vkCreateComputePipelines C.PFN_vkCreateComputePipelines - vkCreateFence C.PFN_vkCreateFence - vkDestroyFence C.PFN_vkDestroyFence - vkWaitForFences C.PFN_vkWaitForFences - vkResetFences C.PFN_vkResetFences - vkGetPhysicalDeviceProperties C.PFN_vkGetPhysicalDeviceProperties - - vkGetPhysicalDeviceSurfaceSupportKHR C.PFN_vkGetPhysicalDeviceSurfaceSupportKHR - vkDestroySurfaceKHR C.PFN_vkDestroySurfaceKHR - vkGetPhysicalDeviceSurfaceFormatsKHR C.PFN_vkGetPhysicalDeviceSurfaceFormatsKHR - vkGetPhysicalDeviceSurfacePresentModesKHR C.PFN_vkGetPhysicalDeviceSurfacePresentModesKHR - vkGetPhysicalDeviceSurfaceCapabilitiesKHR C.PFN_vkGetPhysicalDeviceSurfaceCapabilitiesKHR - - vkCreateSwapchainKHR C.PFN_vkCreateSwapchainKHR - vkDestroySwapchainKHR C.PFN_vkDestroySwapchainKHR - vkGetSwapchainImagesKHR C.PFN_vkGetSwapchainImagesKHR - vkAcquireNextImageKHR C.PFN_vkAcquireNextImageKHR - vkQueuePresentKHR C.PFN_vkQueuePresentKHR -} - -var ( - nilSurface C.VkSurfaceKHR - nilSwapchain C.VkSwapchainKHR - nilSemaphore C.VkSemaphore - nilImageView C.VkImageView - nilRenderPass C.VkRenderPass - nilFramebuffer C.VkFramebuffer - nilCommandPool C.VkCommandPool - nilImage C.VkImage - nilDeviceMemory C.VkDeviceMemory - nilSampler C.VkSampler - nilBuffer C.VkBuffer - nilShaderModule C.VkShaderModule - nilPipeline C.VkPipeline - nilPipelineCache C.VkPipelineCache - nilPipelineLayout C.VkPipelineLayout - nilDescriptorSetLayout C.VkDescriptorSetLayout - nilDescriptorPool C.VkDescriptorPool - nilDescriptorSet C.VkDescriptorSet - nilFence C.VkFence -) - -func vkInit() error { - once.Do(func() { - var libName string - switch { - case runtime.GOOS == "android": - libName = "libvulkan.so" - default: - libName = "libvulkan.so.1" - } - lib := dlopen(libName) - if lib == nil { - loadErr = fmt.Errorf("vulkan: %s", C.GoString(C.dlerror())) - return - } - dlopen := func(name string) *[0]byte { - return (*[0]byte)(dlsym(lib, name)) - } - must := func(name string) *[0]byte { - ptr := dlopen(name) - if ptr != nil { - return ptr - } - if loadErr == nil { - loadErr = fmt.Errorf("vulkan: function %q not found: %s", name, C.GoString(C.dlerror())) - } - return nil - } - funcs.vkCreateInstance = must("vkCreateInstance") - funcs.vkDestroyInstance = must("vkDestroyInstance") - funcs.vkEnumeratePhysicalDevices = must("vkEnumeratePhysicalDevices") - funcs.vkGetPhysicalDeviceQueueFamilyProperties = must("vkGetPhysicalDeviceQueueFamilyProperties") - funcs.vkGetPhysicalDeviceFormatProperties = must("vkGetPhysicalDeviceFormatProperties") - funcs.vkCreateDevice = must("vkCreateDevice") - funcs.vkDestroyDevice = must("vkDestroyDevice") - funcs.vkGetDeviceQueue = must("vkGetDeviceQueue") - funcs.vkCreateImageView = must("vkCreateImageView") - funcs.vkDestroyImageView = must("vkDestroyImageView") - funcs.vkCreateFramebuffer = must("vkCreateFramebuffer") - funcs.vkDestroyFramebuffer = must("vkDestroyFramebuffer") - funcs.vkDeviceWaitIdle = must("vkDeviceWaitIdle") - funcs.vkQueueWaitIdle = must("vkQueueWaitIdle") - funcs.vkCreateSemaphore = must("vkCreateSemaphore") - funcs.vkDestroySemaphore = must("vkDestroySemaphore") - funcs.vkCreateRenderPass = must("vkCreateRenderPass") - funcs.vkDestroyRenderPass = must("vkDestroyRenderPass") - funcs.vkCreateCommandPool = must("vkCreateCommandPool") - funcs.vkDestroyCommandPool = must("vkDestroyCommandPool") - funcs.vkAllocateCommandBuffers = must("vkAllocateCommandBuffers") - funcs.vkFreeCommandBuffers = must("vkFreeCommandBuffers") - funcs.vkBeginCommandBuffer = must("vkBeginCommandBuffer") - funcs.vkEndCommandBuffer = must("vkEndCommandBuffer") - funcs.vkQueueSubmit = must("vkQueueSubmit") - funcs.vkCmdBeginRenderPass = must("vkCmdBeginRenderPass") - funcs.vkCmdEndRenderPass = must("vkCmdEndRenderPass") - funcs.vkCmdCopyBuffer = must("vkCmdCopyBuffer") - funcs.vkCmdCopyBufferToImage = must("vkCmdCopyBufferToImage") - funcs.vkCmdPipelineBarrier = must("vkCmdPipelineBarrier") - funcs.vkCmdPushConstants = must("vkCmdPushConstants") - funcs.vkCmdBindPipeline = must("vkCmdBindPipeline") - funcs.vkCmdBindVertexBuffers = must("vkCmdBindVertexBuffers") - funcs.vkCmdSetViewport = must("vkCmdSetViewport") - funcs.vkCmdBindIndexBuffer = must("vkCmdBindIndexBuffer") - funcs.vkCmdDraw = must("vkCmdDraw") - funcs.vkCmdDrawIndexed = must("vkCmdDrawIndexed") - funcs.vkCmdBindDescriptorSets = must("vkCmdBindDescriptorSets") - funcs.vkCmdCopyImageToBuffer = must("vkCmdCopyImageToBuffer") - funcs.vkCmdDispatch = must("vkCmdDispatch") - funcs.vkCreateImage = must("vkCreateImage") - funcs.vkDestroyImage = must("vkDestroyImage") - funcs.vkGetImageMemoryRequirements = must("vkGetImageMemoryRequirements") - funcs.vkAllocateMemory = must("vkAllocateMemory") - funcs.vkBindImageMemory = must("vkBindImageMemory") - funcs.vkFreeMemory = must("vkFreeMemory") - funcs.vkGetPhysicalDeviceMemoryProperties = must("vkGetPhysicalDeviceMemoryProperties") - funcs.vkCreateSampler = must("vkCreateSampler") - funcs.vkDestroySampler = must("vkDestroySampler") - funcs.vkCreateBuffer = must("vkCreateBuffer") - funcs.vkDestroyBuffer = must("vkDestroyBuffer") - funcs.vkGetBufferMemoryRequirements = must("vkGetBufferMemoryRequirements") - funcs.vkBindBufferMemory = must("vkBindBufferMemory") - funcs.vkCreateShaderModule = must("vkCreateShaderModule") - funcs.vkDestroyShaderModule = must("vkDestroyShaderModule") - funcs.vkCreateGraphicsPipelines = must("vkCreateGraphicsPipelines") - funcs.vkDestroyPipeline = must("vkDestroyPipeline") - funcs.vkCreatePipelineLayout = must("vkCreatePipelineLayout") - funcs.vkDestroyPipelineLayout = must("vkDestroyPipelineLayout") - funcs.vkCreateDescriptorSetLayout = must("vkCreateDescriptorSetLayout") - funcs.vkDestroyDescriptorSetLayout = must("vkDestroyDescriptorSetLayout") - funcs.vkMapMemory = must("vkMapMemory") - funcs.vkUnmapMemory = must("vkUnmapMemory") - funcs.vkResetCommandBuffer = must("vkResetCommandBuffer") - funcs.vkCreateDescriptorPool = must("vkCreateDescriptorPool") - funcs.vkDestroyDescriptorPool = must("vkDestroyDescriptorPool") - funcs.vkAllocateDescriptorSets = must("vkAllocateDescriptorSets") - funcs.vkFreeDescriptorSets = must("vkFreeDescriptorSets") - funcs.vkUpdateDescriptorSets = must("vkUpdateDescriptorSets") - funcs.vkResetDescriptorPool = must("vkResetDescriptorPool") - funcs.vkCmdBlitImage = must("vkCmdBlitImage") - funcs.vkCmdCopyImage = must("vkCmdCopyImage") - funcs.vkCreateComputePipelines = must("vkCreateComputePipelines") - funcs.vkCreateFence = must("vkCreateFence") - funcs.vkDestroyFence = must("vkDestroyFence") - funcs.vkWaitForFences = must("vkWaitForFences") - funcs.vkResetFences = must("vkResetFences") - funcs.vkGetPhysicalDeviceProperties = must("vkGetPhysicalDeviceProperties") - - funcs.vkGetPhysicalDeviceSurfaceSupportKHR = dlopen("vkGetPhysicalDeviceSurfaceSupportKHR") - funcs.vkDestroySurfaceKHR = dlopen("vkDestroySurfaceKHR") - funcs.vkGetPhysicalDeviceSurfaceFormatsKHR = dlopen("vkGetPhysicalDeviceSurfaceFormatsKHR") - funcs.vkGetPhysicalDeviceSurfacePresentModesKHR = dlopen("vkGetPhysicalDeviceSurfacePresentModesKHR") - funcs.vkGetPhysicalDeviceSurfaceCapabilitiesKHR = dlopen("vkGetPhysicalDeviceSurfaceCapabilitiesKHR") - - funcs.vkCreateSwapchainKHR = dlopen("vkCreateSwapchainKHR") - funcs.vkDestroySwapchainKHR = dlopen("vkDestroySwapchainKHR") - funcs.vkGetSwapchainImagesKHR = dlopen("vkGetSwapchainImagesKHR") - funcs.vkAcquireNextImageKHR = dlopen("vkAcquireNextImageKHR") - funcs.vkQueuePresentKHR = dlopen("vkQueuePresentKHR") - - for _, f := range loadFuncs { - f(dlopen) - } - }) - return loadErr -} - -func CreateInstance(exts ...string) (Instance, error) { - if err := vkInit(); err != nil { - return nil, err - } - // VK_MAKE_API_VERSION macro. - makeVer := func(variant, major, minor, patch int) C.uint32_t { - return ((((C.uint32_t)(variant)) << 29) | (((C.uint32_t)(major)) << 22) | (((C.uint32_t)(minor)) << 12) | ((C.uint32_t)(patch))) - } - appInf := C.VkApplicationInfo{ - sType: C.VK_STRUCTURE_TYPE_APPLICATION_INFO, - apiVersion: makeVer(0, 1, 0, 0), - } - inf := C.VkInstanceCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO, - // pApplicationInfo may be omitted according to the spec, but the Android - // emulator crashes without it. - pApplicationInfo: &appInf, - } - if len(exts) > 0 { - cexts := mallocCStringArr(exts) - defer freeCStringArr(cexts) - inf.enabledExtensionCount = C.uint32_t(len(exts)) - inf.ppEnabledExtensionNames = &cexts[0] - } - var inst Instance - if err := vkErr(C.vkCreateInstance(funcs.vkCreateInstance, inf, nil, &inst)); err != nil { - return nil, fmt.Errorf("vulkan: vkCreateInstance: %w", err) - } - return inst, nil -} - -func mallocCStringArr(s []string) []*C.char { - carr := make([]*C.char, len(s)) - for i, ext := range s { - carr[i] = C.CString(ext) - } - return carr -} - -func freeCStringArr(s []*C.char) { - for i := range s { - C.free(unsafe.Pointer(s[i])) - s[i] = nil - } -} - -func DestroyInstance(inst Instance) { - C.vkDestroyInstance(funcs.vkDestroyInstance, inst, nil) -} - -func GetPhysicalDeviceQueueFamilyProperties(pd PhysicalDevice) []QueueFamilyProperties { - var count C.uint32_t - C.vkGetPhysicalDeviceQueueFamilyProperties(funcs.vkGetPhysicalDeviceQueueFamilyProperties, pd, &count, nil) - if count == 0 { - return nil - } - queues := make([]C.VkQueueFamilyProperties, count) - C.vkGetPhysicalDeviceQueueFamilyProperties(funcs.vkGetPhysicalDeviceQueueFamilyProperties, pd, &count, &queues[0]) - return queues -} - -func EnumeratePhysicalDevices(inst Instance) ([]PhysicalDevice, error) { - var count C.uint32_t - if err := vkErr(C.vkEnumeratePhysicalDevices(funcs.vkEnumeratePhysicalDevices, inst, &count, nil)); err != nil { - return nil, fmt.Errorf("vulkan: vkEnumeratePhysicalDevices: %w", err) - } - if count == 0 { - return nil, nil - } - devs := make([]C.VkPhysicalDevice, count) - if err := vkErr(C.vkEnumeratePhysicalDevices(funcs.vkEnumeratePhysicalDevices, inst, &count, &devs[0])); err != nil { - return nil, fmt.Errorf("vulkan: vkEnumeratePhysicalDevices: %w", err) - } - return devs, nil -} - -func ChoosePhysicalDevice(inst Instance, surf Surface) (PhysicalDevice, int, error) { - devs, err := EnumeratePhysicalDevices(inst) - if err != nil { - return nil, 0, err - } - for _, pd := range devs { - var props C.VkPhysicalDeviceProperties - C.vkGetPhysicalDeviceProperties(funcs.vkGetPhysicalDeviceProperties, pd, &props) - // The lavapipe software implementation doesn't work well rendering to a surface. - // See https://gitlab.freedesktop.org/mesa/mesa/-/issues/5473. - if surf != 0 && props.deviceType == C.VK_PHYSICAL_DEVICE_TYPE_CPU { - continue - } - const caps = C.VK_QUEUE_GRAPHICS_BIT | C.VK_QUEUE_COMPUTE_BIT - queueIdx, ok, err := chooseQueue(pd, surf, caps) - if err != nil { - return nil, 0, err - } - if !ok { - continue - } - if surf != nilSurface { - _, fmtFound, err := chooseFormat(pd, surf) - if err != nil { - return nil, 0, err - } - _, modFound, err := choosePresentMode(pd, surf) - if err != nil { - return nil, 0, err - } - if !fmtFound || !modFound { - continue - } - } - return pd, queueIdx, nil - } - return nil, 0, errors.New("vulkan: no suitable device found") -} - -func CreateDeviceAndQueue(pd C.VkPhysicalDevice, queueIdx int, exts ...string) (Device, error) { - priority := C.float(1.0) - qinf := C.VkDeviceQueueCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO, - queueCount: 1, - queueFamilyIndex: C.uint32_t(queueIdx), - pQueuePriorities: &priority, - } - inf := C.VkDeviceCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, - queueCreateInfoCount: 1, - enabledExtensionCount: C.uint32_t(len(exts)), - } - if len(exts) > 0 { - cexts := mallocCStringArr(exts) - defer freeCStringArr(cexts) - inf.ppEnabledExtensionNames = &cexts[0] - } - var dev Device - if err := vkErr(C.vkCreateDevice(funcs.vkCreateDevice, pd, inf, qinf, nil, &dev)); err != nil { - return nil, fmt.Errorf("vulkan: vkCreateDevice: %w", err) - } - return dev, nil -} - -func GetDeviceQueue(d Device, queueFamily, queueIndex int) Queue { - var queue Queue - C.vkGetDeviceQueue(funcs.vkGetDeviceQueue, d, C.uint32_t(queueFamily), C.uint32_t(queueIndex), &queue) - return queue -} - -func GetPhysicalDeviceSurfaceCapabilities(pd PhysicalDevice, surf Surface) (SurfaceCapabilities, error) { - var caps C.VkSurfaceCapabilitiesKHR - err := vkErr(C.vkGetPhysicalDeviceSurfaceCapabilitiesKHR(funcs.vkGetPhysicalDeviceSurfaceCapabilitiesKHR, pd, surf, &caps)) - if err != nil { - return SurfaceCapabilities{}, fmt.Errorf("vulkan: vkGetPhysicalDeviceSurfaceCapabilitiesKHR: %w", err) - } - return caps, nil -} - -func CreateSwapchain(pd PhysicalDevice, d Device, surf Surface, width, height int, old Swapchain) (Swapchain, []Image, Format, error) { - caps, err := GetPhysicalDeviceSurfaceCapabilities(pd, surf) - if err != nil { - return nilSwapchain, nil, 0, err - } - mode, modeOK, err := choosePresentMode(pd, surf) - if err != nil { - return nilSwapchain, nil, 0, err - } - format, fmtOK, err := chooseFormat(pd, surf) - if err != nil { - return nilSwapchain, nil, 0, err - } - if !modeOK || !fmtOK { - // This shouldn't happen because CreateDeviceAndQueue found at least - // one valid format and present mode. - return nilSwapchain, nil, 0, errors.New("vulkan: no valid format and present mode found") - } - // Find supported alpha composite mode. It doesn't matter which one, because rendering is - // always opaque. - alphaComp := C.VkCompositeAlphaFlagBitsKHR(1) - for caps.supportedCompositeAlpha&C.VkCompositeAlphaFlagsKHR(alphaComp) == 0 { - alphaComp <<= 1 - } - trans := C.VkSurfaceTransformFlagBitsKHR(C.VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) - if caps.supportedTransforms&C.VkSurfaceTransformFlagsKHR(trans) == 0 { - return nilSwapchain, nil, 0, errors.New("vulkan: VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR not supported") - } - inf := C.VkSwapchainCreateInfoKHR{ - sType: C.VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, - surface: surf, - minImageCount: caps.minImageCount, - imageFormat: format.format, - imageColorSpace: format.colorSpace, - imageExtent: C.VkExtent2D{width: C.uint32_t(width), height: C.uint32_t(height)}, - imageArrayLayers: 1, - imageUsage: C.VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, - imageSharingMode: C.VK_SHARING_MODE_EXCLUSIVE, - preTransform: trans, - presentMode: mode, - compositeAlpha: C.VkCompositeAlphaFlagBitsKHR(alphaComp), - clipped: C.VK_TRUE, - oldSwapchain: old, - } - var swchain Swapchain - if err := vkErr(C.vkCreateSwapchainKHR(funcs.vkCreateSwapchainKHR, d, &inf, nil, &swchain)); err != nil { - return nilSwapchain, nil, 0, fmt.Errorf("vulkan: vkCreateSwapchainKHR: %w", err) - } - var count C.uint32_t - if err := vkErr(C.vkGetSwapchainImagesKHR(funcs.vkGetSwapchainImagesKHR, d, swchain, &count, nil)); err != nil { - DestroySwapchain(d, swchain) - return nilSwapchain, nil, 0, fmt.Errorf("vulkan: vkGetSwapchainImagesKHR: %w", err) - } - if count == 0 { - DestroySwapchain(d, swchain) - return nilSwapchain, nil, 0, errors.New("vulkan: vkGetSwapchainImagesKHR returned no images") - } - imgs := make([]Image, count) - if err := vkErr(C.vkGetSwapchainImagesKHR(funcs.vkGetSwapchainImagesKHR, d, swchain, &count, &imgs[0])); err != nil { - DestroySwapchain(d, swchain) - return nilSwapchain, nil, 0, fmt.Errorf("vulkan: vkGetSwapchainImagesKHR: %w", err) - } - return swchain, imgs, format.format, nil -} - -func DestroySwapchain(d Device, swchain Swapchain) { - C.vkDestroySwapchainKHR(funcs.vkDestroySwapchainKHR, d, swchain, nil) -} - -func AcquireNextImage(d Device, swchain Swapchain, sem Semaphore, fence Fence) (int, error) { - res := C.vkAcquireNextImageKHR(funcs.vkAcquireNextImageKHR, d, swchain, math.MaxUint64, sem, fence) - if err := vkErr(res.res); err != nil { - return 0, fmt.Errorf("vulkan: vkAcquireNextImageKHR: %w", err) - } - return int(res.uint), nil -} - -func PresentQueue(q Queue, swchain Swapchain, sem Semaphore, imgIdx int) error { - cidx := C.uint32_t(imgIdx) - inf := C.VkPresentInfoKHR{ - sType: C.VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, - swapchainCount: 1, - pSwapchains: &swchain, - pImageIndices: &cidx, - } - if sem != nilSemaphore { - inf.waitSemaphoreCount = 1 - inf.pWaitSemaphores = &sem - } - if err := vkErr(C.vkQueuePresentKHR(funcs.vkQueuePresentKHR, q, inf)); err != nil { - return fmt.Errorf("vulkan: vkQueuePresentKHR: %w", err) - } - return nil -} - -func CreateImageView(d Device, img Image, format Format) (ImageView, error) { - inf := C.VkImageViewCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - image: img, - viewType: C.VK_IMAGE_VIEW_TYPE_2D, - format: format, - subresourceRange: C.VkImageSubresourceRange{ - aspectMask: C.VK_IMAGE_ASPECT_COLOR_BIT, - levelCount: C.VK_REMAINING_MIP_LEVELS, - layerCount: C.VK_REMAINING_ARRAY_LAYERS, - }, - } - var view C.VkImageView - if err := vkErr(C.vkCreateImageView(funcs.vkCreateImageView, d, &inf, nil, &view)); err != nil { - return nilImageView, fmt.Errorf("vulkan: vkCreateImageView: %w", err) - } - return view, nil -} - -func DestroyImageView(d Device, view ImageView) { - C.vkDestroyImageView(funcs.vkDestroyImageView, d, view, nil) -} - -func CreateRenderPass(d Device, format Format, loadOp AttachmentLoadOp, initialLayout, finalLayout ImageLayout, passDeps []SubpassDependency) (RenderPass, error) { - att := C.VkAttachmentDescription{ - format: format, - samples: C.VK_SAMPLE_COUNT_1_BIT, - loadOp: loadOp, - storeOp: C.VK_ATTACHMENT_STORE_OP_STORE, - stencilLoadOp: C.VK_ATTACHMENT_LOAD_OP_DONT_CARE, - stencilStoreOp: C.VK_ATTACHMENT_STORE_OP_DONT_CARE, - initialLayout: initialLayout, - finalLayout: finalLayout, - } - - ref := C.VkAttachmentReference{ - attachment: 0, - layout: C.VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, - } - - sub := C.VkSubpassDescription{ - pipelineBindPoint: C.VK_PIPELINE_BIND_POINT_GRAPHICS, - colorAttachmentCount: 1, - pColorAttachments: &ref, - } - - inf := C.VkRenderPassCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, - attachmentCount: 1, - pAttachments: &att, - subpassCount: 1, - } - if n := len(passDeps); n > 0 { - inf.dependencyCount = C.uint32_t(n) - inf.pDependencies = &passDeps[0] - } - - var pass RenderPass - if err := vkErr(C.vkCreateRenderPass(funcs.vkCreateRenderPass, d, inf, sub, nil, &pass)); err != nil { - return nilRenderPass, fmt.Errorf("vulkan: vkCreateRenderPass: %w", err) - } - return pass, nil -} - -func DestroyRenderPass(d Device, r RenderPass) { - C.vkDestroyRenderPass(funcs.vkDestroyRenderPass, d, r, nil) -} - -func CreateFramebuffer(d Device, rp RenderPass, view ImageView, width, height int) (Framebuffer, error) { - inf := C.VkFramebufferCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO, - renderPass: rp, - attachmentCount: 1, - pAttachments: &view, - width: C.uint32_t(width), - height: C.uint32_t(height), - layers: 1, - } - var fbo Framebuffer - if err := vkErr(C.vkCreateFramebuffer(funcs.vkCreateFramebuffer, d, inf, nil, &fbo)); err != nil { - return nilFramebuffer, fmt.Errorf("vulkan: vkCreateFramebuffer: %w", err) - } - return fbo, nil -} - -func DestroyFramebuffer(d Device, f Framebuffer) { - C.vkDestroyFramebuffer(funcs.vkDestroyFramebuffer, d, f, nil) -} - -func DeviceWaitIdle(d Device) error { - if err := vkErr(C.vkDeviceWaitIdle(funcs.vkDeviceWaitIdle, d)); err != nil { - return fmt.Errorf("vulkan: vkDeviceWaitIdle: %w", err) - } - return nil -} - -func QueueWaitIdle(q Queue) error { - if err := vkErr(C.vkQueueWaitIdle(funcs.vkQueueWaitIdle, q)); err != nil { - return fmt.Errorf("vulkan: vkQueueWaitIdle: %w", err) - } - return nil -} - -func CreateSemaphore(d Device) (Semaphore, error) { - inf := C.VkSemaphoreCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, - } - var sem Semaphore - err := vkErr(C.vkCreateSemaphore(funcs.vkCreateSemaphore, d, &inf, nil, &sem)) - if err != nil { - return nilSemaphore, fmt.Errorf("vulkan: vkCreateSemaphore: %w", err) - } - return sem, err -} - -func DestroySemaphore(d Device, sem Semaphore) { - C.vkDestroySemaphore(funcs.vkDestroySemaphore, d, sem, nil) -} - -func DestroyDevice(dev Device) { - C.vkDestroyDevice(funcs.vkDestroyDevice, dev, nil) -} - -func DestroySurface(inst Instance, s Surface) { - C.vkDestroySurfaceKHR(funcs.vkDestroySurfaceKHR, inst, s, nil) -} - -func CreateCommandPool(d Device, queueIndex int) (CommandPool, error) { - inf := C.VkCommandPoolCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO, - queueFamilyIndex: C.uint32_t(queueIndex), - flags: C.VK_COMMAND_POOL_CREATE_TRANSIENT_BIT | C.VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, - } - - var pool CommandPool - if err := vkErr(C.vkCreateCommandPool(funcs.vkCreateCommandPool, d, &inf, nil, &pool)); err != nil { - return nilCommandPool, fmt.Errorf("vulkan: vkCreateCommandPool: %w", err) - } - return pool, nil -} - -func DestroyCommandPool(d Device, pool CommandPool) { - C.vkDestroyCommandPool(funcs.vkDestroyCommandPool, d, pool, nil) -} - -func AllocateCommandBuffer(d Device, pool CommandPool) (CommandBuffer, error) { - inf := C.VkCommandBufferAllocateInfo{ - sType: C.VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, - commandPool: pool, - level: C.VK_COMMAND_BUFFER_LEVEL_PRIMARY, - commandBufferCount: 1, - } - - var buf CommandBuffer - if err := vkErr(C.vkAllocateCommandBuffers(funcs.vkAllocateCommandBuffers, d, &inf, &buf)); err != nil { - return nil, fmt.Errorf("vulkan: vkAllocateCommandBuffers: %w", err) - } - return buf, nil -} - -func FreeCommandBuffers(d Device, pool CommandPool, bufs ...CommandBuffer) { - if len(bufs) == 0 { - return - } - C.vkFreeCommandBuffers(funcs.vkFreeCommandBuffers, d, pool, C.uint32_t(len(bufs)), &bufs[0]) -} - -func BeginCommandBuffer(buf CommandBuffer) error { - inf := C.VkCommandBufferBeginInfo{ - sType: C.VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO, - flags: C.VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT, - } - if err := vkErr(C.vkBeginCommandBuffer(funcs.vkBeginCommandBuffer, buf, inf)); err != nil { - return fmt.Errorf("vulkan: vkBeginCommandBuffer: %w", err) - } - return nil -} - -func EndCommandBuffer(buf CommandBuffer) error { - if err := vkErr(C.vkEndCommandBuffer(funcs.vkEndCommandBuffer, buf)); err != nil { - return fmt.Errorf("vulkan: vkEndCommandBuffer: %w", err) - } - return nil -} - -func QueueSubmit(q Queue, buf CommandBuffer, waitSems []Semaphore, waitStages []PipelineStageFlags, sigSems []Semaphore, fence Fence) error { - inf := C.VkSubmitInfo{ - sType: C.VK_STRUCTURE_TYPE_SUBMIT_INFO, - commandBufferCount: 1, - pCommandBuffers: &buf, - } - if len(waitSems) > 0 { - if len(waitSems) != len(waitStages) { - panic("len(waitSems) != len(waitStages)") - } - inf.waitSemaphoreCount = C.uint32_t(len(waitSems)) - inf.pWaitSemaphores = &waitSems[0] - inf.pWaitDstStageMask = &waitStages[0] - } - if len(sigSems) > 0 { - inf.signalSemaphoreCount = C.uint32_t(len(sigSems)) - inf.pSignalSemaphores = &sigSems[0] - } - if err := vkErr(C.vkQueueSubmit(funcs.vkQueueSubmit, q, inf, fence)); err != nil { - return fmt.Errorf("vulkan: vkQueueSubmit: %w", err) - } - return nil -} - -func CmdBeginRenderPass(buf CommandBuffer, rp RenderPass, fbo Framebuffer, width, height int, clearCol [4]float32) { - cclearCol := [4]C.float{C.float(clearCol[0]), C.float(clearCol[1]), C.float(clearCol[2]), C.float(clearCol[3])} - inf := C.VkRenderPassBeginInfo{ - sType: C.VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, - renderPass: rp, - framebuffer: fbo, - renderArea: C.VkRect2D{extent: C.VkExtent2D{width: C.uint32_t(width), height: C.uint32_t(height)}}, - clearValueCount: 1, - pClearValues: (*C.VkClearValue)(unsafe.Pointer(&cclearCol)), - } - C.vkCmdBeginRenderPass(funcs.vkCmdBeginRenderPass, buf, inf, C.VK_SUBPASS_CONTENTS_INLINE) -} - -func CmdEndRenderPass(buf CommandBuffer) { - C.vkCmdEndRenderPass(funcs.vkCmdEndRenderPass, buf) -} - -func CmdCopyBuffer(cmdBuf CommandBuffer, src, dst Buffer, srcOff, dstOff, size int) { - C.vkCmdCopyBuffer(funcs.vkCmdCopyBuffer, cmdBuf, src, dst, 1, &C.VkBufferCopy{ - srcOffset: C.VkDeviceSize(srcOff), - dstOffset: C.VkDeviceSize(dstOff), - size: C.VkDeviceSize(size), - }) -} - -func CmdCopyBufferToImage(cmdBuf CommandBuffer, src Buffer, dst Image, layout ImageLayout, copy BufferImageCopy) { - C.vkCmdCopyBufferToImage(funcs.vkCmdCopyBufferToImage, cmdBuf, src, dst, layout, 1, ©) -} - -func CmdPipelineBarrier(cmdBuf CommandBuffer, srcStage, dstStage PipelineStageFlags, flags DependencyFlags, memBarriers []MemoryBarrier, bufBarriers []BufferMemoryBarrier, imgBarriers []ImageMemoryBarrier) { - var memPtr *MemoryBarrier - if len(memBarriers) > 0 { - memPtr = &memBarriers[0] - } - var bufPtr *BufferMemoryBarrier - if len(bufBarriers) > 0 { - bufPtr = &bufBarriers[0] - } - var imgPtr *ImageMemoryBarrier - if len(imgBarriers) > 0 { - imgPtr = &imgBarriers[0] - } - C.vkCmdPipelineBarrier(funcs.vkCmdPipelineBarrier, cmdBuf, srcStage, dstStage, flags, - C.uint32_t(len(memBarriers)), memPtr, - C.uint32_t(len(bufBarriers)), bufPtr, - C.uint32_t(len(imgBarriers)), imgPtr) -} - -func CmdPushConstants(cmdBuf CommandBuffer, layout PipelineLayout, stages ShaderStageFlags, offset int, data []byte) { - if len(data) == 0 { - return - } - C.vkCmdPushConstants(funcs.vkCmdPushConstants, cmdBuf, layout, stages, C.uint32_t(offset), C.uint32_t(len(data)), unsafe.Pointer(&data[0])) -} - -func CmdBindPipeline(cmdBuf CommandBuffer, bindPoint PipelineBindPoint, pipe Pipeline) { - C.vkCmdBindPipeline(funcs.vkCmdBindPipeline, cmdBuf, bindPoint, pipe) -} - -func CmdBindVertexBuffers(cmdBuf CommandBuffer, first int, buffers []Buffer, sizes []DeviceSize) { - if len(buffers) == 0 { - return - } - C.vkCmdBindVertexBuffers(funcs.vkCmdBindVertexBuffers, cmdBuf, C.uint32_t(first), C.uint32_t(len(buffers)), &buffers[0], &sizes[0]) -} - -func CmdSetViewport(cmdBuf CommandBuffer, first int, viewports ...Viewport) { - if len(viewports) == 0 { - return - } - C.vkCmdSetViewport(funcs.vkCmdSetViewport, cmdBuf, C.uint32_t(first), C.uint32_t(len(viewports)), &viewports[0]) -} - -func CmdBindIndexBuffer(cmdBuf CommandBuffer, buffer Buffer, offset int, typ IndexType) { - C.vkCmdBindIndexBuffer(funcs.vkCmdBindIndexBuffer, cmdBuf, buffer, C.VkDeviceSize(offset), typ) -} - -func CmdDraw(cmdBuf CommandBuffer, vertCount, instCount, firstVert, firstInst int) { - C.vkCmdDraw(funcs.vkCmdDraw, cmdBuf, C.uint32_t(vertCount), C.uint32_t(instCount), C.uint32_t(firstVert), C.uint32_t(firstInst)) -} - -func CmdDrawIndexed(cmdBuf CommandBuffer, idxCount, instCount, firstIdx, vertOff, firstInst int) { - C.vkCmdDrawIndexed(funcs.vkCmdDrawIndexed, cmdBuf, C.uint32_t(idxCount), C.uint32_t(instCount), C.uint32_t(firstIdx), C.int32_t(vertOff), C.uint32_t(firstInst)) -} - -func GetPhysicalDeviceFormatProperties(physDev PhysicalDevice, format Format) FormatFeatureFlags { - var props C.VkFormatProperties - C.vkGetPhysicalDeviceFormatProperties(funcs.vkGetPhysicalDeviceFormatProperties, physDev, format, &props) - return FormatFeatureFlags(props.optimalTilingFeatures) -} - -func CmdBindDescriptorSets(cmdBuf CommandBuffer, point PipelineBindPoint, layout PipelineLayout, firstSet int, sets []DescriptorSet) { - C.vkCmdBindDescriptorSets(funcs.vkCmdBindDescriptorSets, cmdBuf, point, layout, C.uint32_t(firstSet), C.uint32_t(len(sets)), &sets[0], 0, nil) -} - -func CmdBlitImage(cmdBuf CommandBuffer, src Image, srcLayout ImageLayout, dst Image, dstLayout ImageLayout, regions []ImageBlit, filter Filter) { - if len(regions) == 0 { - return - } - C.vkCmdBlitImage(funcs.vkCmdBlitImage, cmdBuf, src, srcLayout, dst, dstLayout, C.uint32_t(len(regions)), ®ions[0], filter) -} - -func CmdCopyImage(cmdBuf CommandBuffer, src Image, srcLayout ImageLayout, dst Image, dstLayout ImageLayout, regions []ImageCopy) { - if len(regions) == 0 { - return - } - C.vkCmdCopyImage(funcs.vkCmdCopyImage, cmdBuf, src, srcLayout, dst, dstLayout, C.uint32_t(len(regions)), ®ions[0]) -} - -func CmdCopyImageToBuffer(cmdBuf CommandBuffer, src Image, srcLayout ImageLayout, dst Buffer, regions []BufferImageCopy) { - if len(regions) == 0 { - return - } - C.vkCmdCopyImageToBuffer(funcs.vkCmdCopyImageToBuffer, cmdBuf, src, srcLayout, dst, C.uint32_t(len(regions)), ®ions[0]) -} - -func CmdDispatch(cmdBuf CommandBuffer, x, y, z int) { - C.vkCmdDispatch(funcs.vkCmdDispatch, cmdBuf, C.uint32_t(x), C.uint32_t(y), C.uint32_t(z)) -} - -func CreateImage(pd PhysicalDevice, d Device, format Format, width, height, mipmaps int, usage ImageUsageFlags) (Image, DeviceMemory, error) { - inf := C.VkImageCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - imageType: C.VK_IMAGE_TYPE_2D, - format: format, - extent: C.VkExtent3D{ - width: C.uint32_t(width), - height: C.uint32_t(height), - depth: 1, - }, - mipLevels: C.uint32_t(mipmaps), - arrayLayers: 1, - samples: C.VK_SAMPLE_COUNT_1_BIT, - tiling: C.VK_IMAGE_TILING_OPTIMAL, - usage: usage, - initialLayout: C.VK_IMAGE_LAYOUT_UNDEFINED, - } - var img C.VkImage - if err := vkErr(C.vkCreateImage(funcs.vkCreateImage, d, &inf, nil, &img)); err != nil { - return nilImage, nilDeviceMemory, fmt.Errorf("vulkan: vkCreateImage: %w", err) - } - var memReqs C.VkMemoryRequirements - C.vkGetImageMemoryRequirements(funcs.vkGetImageMemoryRequirements, d, img, &memReqs) - - memIdx, found := findMemoryTypeIndex(pd, memReqs.memoryTypeBits, C.VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) - if !found { - DestroyImage(d, img) - return nilImage, nilDeviceMemory, errors.New("vulkan: no memory type suitable for images found") - } - - memInf := C.VkMemoryAllocateInfo{ - sType: C.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - allocationSize: memReqs.size, - memoryTypeIndex: C.uint32_t(memIdx), - } - var imgMem C.VkDeviceMemory - if err := vkErr(C.vkAllocateMemory(funcs.vkAllocateMemory, d, &memInf, nil, &imgMem)); err != nil { - DestroyImage(d, img) - return nilImage, nilDeviceMemory, fmt.Errorf("vulkan: vkAllocateMemory: %w", err) - } - - if err := vkErr(C.vkBindImageMemory(funcs.vkBindImageMemory, d, img, imgMem, 0)); err != nil { - FreeMemory(d, imgMem) - DestroyImage(d, img) - return nilImage, nilDeviceMemory, fmt.Errorf("vulkan: vkBindImageMemory: %w", err) - } - return img, imgMem, nil -} - -func DestroyImage(d Device, img Image) { - C.vkDestroyImage(funcs.vkDestroyImage, d, img, nil) -} - -func FreeMemory(d Device, mem DeviceMemory) { - C.vkFreeMemory(funcs.vkFreeMemory, d, mem, nil) -} - -func CreateSampler(d Device, minFilter, magFilter Filter, mipmapMode SamplerMipmapMode) (Sampler, error) { - inf := C.VkSamplerCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, - minFilter: minFilter, - magFilter: magFilter, - mipmapMode: mipmapMode, - maxLod: C.VK_LOD_CLAMP_NONE, - addressModeU: C.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - addressModeV: C.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, - } - var s C.VkSampler - if err := vkErr(C.vkCreateSampler(funcs.vkCreateSampler, d, &inf, nil, &s)); err != nil { - return nilSampler, fmt.Errorf("vulkan: vkCreateSampler: %w", err) - } - return s, nil -} - -func DestroySampler(d Device, sampler Sampler) { - C.vkDestroySampler(funcs.vkDestroySampler, d, sampler, nil) -} - -func CreateBuffer(pd PhysicalDevice, d Device, size int, usage BufferUsageFlags, props MemoryPropertyFlags) (Buffer, DeviceMemory, error) { - inf := C.VkBufferCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, - size: C.VkDeviceSize(size), - usage: usage, - } - var buf C.VkBuffer - if err := vkErr(C.vkCreateBuffer(funcs.vkCreateBuffer, d, &inf, nil, &buf)); err != nil { - return nilBuffer, nilDeviceMemory, fmt.Errorf("vulkan: vkCreateBuffer: %w", err) - } - - var memReqs C.VkMemoryRequirements - C.vkGetBufferMemoryRequirements(funcs.vkGetBufferMemoryRequirements, d, buf, &memReqs) - - memIdx, found := findMemoryTypeIndex(pd, memReqs.memoryTypeBits, props) - if !found { - DestroyBuffer(d, buf) - return nilBuffer, nilDeviceMemory, errors.New("vulkan: no memory suitable for buffers found") - } - memInf := C.VkMemoryAllocateInfo{ - sType: C.VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, - allocationSize: memReqs.size, - memoryTypeIndex: C.uint32_t(memIdx), - } - - var mem C.VkDeviceMemory - if err := vkErr(C.vkAllocateMemory(funcs.vkAllocateMemory, d, &memInf, nil, &mem)); err != nil { - DestroyBuffer(d, buf) - return nilBuffer, nilDeviceMemory, fmt.Errorf("vulkan: vkAllocateMemory: %w", err) - } - - if err := vkErr(C.vkBindBufferMemory(funcs.vkBindBufferMemory, d, buf, mem, 0)); err != nil { - FreeMemory(d, mem) - DestroyBuffer(d, buf) - return nilBuffer, nilDeviceMemory, fmt.Errorf("vulkan: vkBindBufferMemory: %w", err) - } - return buf, mem, nil -} - -func DestroyBuffer(d Device, buf Buffer) { - C.vkDestroyBuffer(funcs.vkDestroyBuffer, d, buf, nil) -} - -func CreateShaderModule(d Device, spirv string) (ShaderModule, error) { - ptr := unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&spirv)).Data) - inf := C.VkShaderModuleCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, - codeSize: C.size_t(len(spirv)), - pCode: (*C.uint32_t)(ptr), - } - - var mod C.VkShaderModule - if err := vkErr(C.vkCreateShaderModule(funcs.vkCreateShaderModule, d, inf, nil, &mod)); err != nil { - return nilShaderModule, fmt.Errorf("vulkan: vkCreateShaderModule: %w", err) - } - return mod, nil -} - -func DestroyShaderModule(d Device, mod ShaderModule) { - C.vkDestroyShaderModule(funcs.vkDestroyShaderModule, d, mod, nil) -} - -func CreateGraphicsPipeline(d Device, pass RenderPass, vmod, fmod ShaderModule, blend bool, srcFactor, dstFactor BlendFactor, topology PrimitiveTopology, bindings []VertexInputBindingDescription, attrs []VertexInputAttributeDescription, layout PipelineLayout) (Pipeline, error) { - main := C.CString("main") - defer C.free(unsafe.Pointer(main)) - stages := []C.VkPipelineShaderStageCreateInfo{ - { - sType: C.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - stage: C.VK_SHADER_STAGE_VERTEX_BIT, - module: vmod, - pName: main, - }, - { - sType: C.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - stage: C.VK_SHADER_STAGE_FRAGMENT_BIT, - module: fmod, - pName: main, - }, - } - dynStates := []C.VkDynamicState{C.VK_DYNAMIC_STATE_VIEWPORT} - dynInf := C.VkPipelineDynamicStateCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, - dynamicStateCount: C.uint32_t(len(dynStates)), - pDynamicStates: &dynStates[0], - } - const maxDim = 0x7fffffff - scissors := []C.VkRect2D{{extent: C.VkExtent2D{width: maxDim, height: maxDim}}} - viewportInf := C.VkPipelineViewportStateCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, - viewportCount: 1, - scissorCount: C.uint32_t(len(scissors)), - pScissors: &scissors[0], - } - enable := C.VkBool32(0) - if blend { - enable = 1 - } - attBlendInf := C.VkPipelineColorBlendAttachmentState{ - blendEnable: enable, - srcColorBlendFactor: srcFactor, - srcAlphaBlendFactor: srcFactor, - dstColorBlendFactor: dstFactor, - dstAlphaBlendFactor: dstFactor, - colorBlendOp: C.VK_BLEND_OP_ADD, - alphaBlendOp: C.VK_BLEND_OP_ADD, - colorWriteMask: C.VK_COLOR_COMPONENT_R_BIT | C.VK_COLOR_COMPONENT_G_BIT | C.VK_COLOR_COMPONENT_B_BIT | C.VK_COLOR_COMPONENT_A_BIT, - } - blendInf := C.VkPipelineColorBlendStateCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, - attachmentCount: 1, - pAttachments: &attBlendInf, - } - var vkBinds []C.VkVertexInputBindingDescription - var vkAttrs []C.VkVertexInputAttributeDescription - for _, b := range bindings { - vkBinds = append(vkBinds, C.VkVertexInputBindingDescription{ - binding: C.uint32_t(b.Binding), - stride: C.uint32_t(b.Stride), - }) - } - for _, a := range attrs { - vkAttrs = append(vkAttrs, C.VkVertexInputAttributeDescription{ - location: C.uint32_t(a.Location), - binding: C.uint32_t(a.Binding), - format: a.Format, - offset: C.uint32_t(a.Offset), - }) - } - vertexInf := C.VkPipelineVertexInputStateCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, - } - if n := len(vkBinds); n > 0 { - vertexInf.vertexBindingDescriptionCount = C.uint32_t(n) - vertexInf.pVertexBindingDescriptions = &vkBinds[0] - } - if n := len(vkAttrs); n > 0 { - vertexInf.vertexAttributeDescriptionCount = C.uint32_t(n) - vertexInf.pVertexAttributeDescriptions = &vkAttrs[0] - } - inf := C.VkGraphicsPipelineCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, - stageCount: C.uint32_t(len(stages)), - pStages: &stages[0], - renderPass: pass, - layout: layout, - pRasterizationState: &C.VkPipelineRasterizationStateCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, - lineWidth: 1.0, - }, - pMultisampleState: &C.VkPipelineMultisampleStateCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, - rasterizationSamples: C.VK_SAMPLE_COUNT_1_BIT, - }, - pInputAssemblyState: &C.VkPipelineInputAssemblyStateCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, - topology: topology, - }, - } - - var pipe C.VkPipeline - if err := vkErr(C.vkCreateGraphicsPipelines(funcs.vkCreateGraphicsPipelines, d, nilPipelineCache, inf, dynInf, blendInf, vertexInf, viewportInf, nil, &pipe)); err != nil { - return nilPipeline, fmt.Errorf("vulkan: vkCreateGraphicsPipelines: %w", err) - } - return pipe, nil -} - -func DestroyPipeline(d Device, p Pipeline) { - C.vkDestroyPipeline(funcs.vkDestroyPipeline, d, p, nil) -} - -func CreatePipelineLayout(d Device, pushRanges []PushConstantRange, sets []DescriptorSetLayout) (PipelineLayout, error) { - inf := C.VkPipelineLayoutCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, - } - if n := len(sets); n > 0 { - inf.setLayoutCount = C.uint32_t(n) - inf.pSetLayouts = &sets[0] - } - if n := len(pushRanges); n > 0 { - inf.pushConstantRangeCount = C.uint32_t(n) - inf.pPushConstantRanges = &pushRanges[0] - } - var l C.VkPipelineLayout - if err := vkErr(C.vkCreatePipelineLayout(funcs.vkCreatePipelineLayout, d, inf, nil, &l)); err != nil { - return nilPipelineLayout, fmt.Errorf("vulkan: vkCreatePipelineLayout: %w", err) - } - return l, nil -} - -func DestroyPipelineLayout(d Device, l PipelineLayout) { - C.vkDestroyPipelineLayout(funcs.vkDestroyPipelineLayout, d, l, nil) -} - -func CreateDescriptorSetLayout(d Device, bindings []DescriptorSetLayoutBinding) (DescriptorSetLayout, error) { - var vkbinds []C.VkDescriptorSetLayoutBinding - for _, b := range bindings { - vkbinds = append(vkbinds, C.VkDescriptorSetLayoutBinding{ - binding: C.uint32_t(b.Binding), - descriptorType: b.DescriptorType, - descriptorCount: 1, - stageFlags: b.StageFlags, - }) - } - inf := C.VkDescriptorSetLayoutCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, - } - if n := len(vkbinds); n > 0 { - inf.bindingCount = C.uint32_t(len(vkbinds)) - inf.pBindings = &vkbinds[0] - } - var l C.VkDescriptorSetLayout - if err := vkErr(C.vkCreateDescriptorSetLayout(funcs.vkCreateDescriptorSetLayout, d, inf, nil, &l)); err != nil { - return nilDescriptorSetLayout, fmt.Errorf("vulkan: vkCreateDescriptorSetLayout: %w", err) - } - return l, nil -} - -func DestroyDescriptorSetLayout(d Device, l DescriptorSetLayout) { - C.vkDestroyDescriptorSetLayout(funcs.vkDestroyDescriptorSetLayout, d, l, nil) -} - -func MapMemory(d Device, mem DeviceMemory, offset, size int) ([]byte, error) { - var ptr unsafe.Pointer - if err := vkErr(C.vkMapMemory(funcs.vkMapMemory, d, mem, C.VkDeviceSize(offset), C.VkDeviceSize(size), 0, &ptr)); err != nil { - return nil, fmt.Errorf("vulkan: vkMapMemory: %w", err) - } - return ((*[1 << 30]byte)(ptr))[:size:size], nil -} - -func UnmapMemory(d Device, mem DeviceMemory) { - C.vkUnmapMemory(funcs.vkUnmapMemory, d, mem) -} - -func ResetCommandBuffer(buf CommandBuffer) error { - if err := vkErr(C.vkResetCommandBuffer(funcs.vkResetCommandBuffer, buf, 0)); err != nil { - return fmt.Errorf("vulkan: vkResetCommandBuffer. %w", err) - } - return nil -} - -func CreateDescriptorPool(d Device, maxSets int, sizes []DescriptorPoolSize) (DescriptorPool, error) { - inf := C.VkDescriptorPoolCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, - maxSets: C.uint32_t(maxSets), - poolSizeCount: C.uint32_t(len(sizes)), - pPoolSizes: &sizes[0], - } - var pool C.VkDescriptorPool - if err := vkErr(C.vkCreateDescriptorPool(funcs.vkCreateDescriptorPool, d, inf, nil, &pool)); err != nil { - return nilDescriptorPool, fmt.Errorf("vulkan: vkCreateDescriptorPool: %w", err) - } - return pool, nil -} - -func DestroyDescriptorPool(d Device, pool DescriptorPool) { - C.vkDestroyDescriptorPool(funcs.vkDestroyDescriptorPool, d, pool, nil) -} - -func ResetDescriptorPool(d Device, pool DescriptorPool) error { - if err := vkErr(C.vkResetDescriptorPool(funcs.vkResetDescriptorPool, d, pool, 0)); err != nil { - return fmt.Errorf("vulkan: vkResetDescriptorPool: %w", err) - } - return nil -} - -func UpdateDescriptorSet(d Device, write WriteDescriptorSet) { - C.vkUpdateDescriptorSets(funcs.vkUpdateDescriptorSets, d, write, 0, nil) -} - -func AllocateDescriptorSets(d Device, pool DescriptorPool, layout DescriptorSetLayout, count int) ([]DescriptorSet, error) { - layouts := make([]DescriptorSetLayout, count) - for i := range layouts { - layouts[i] = layout - } - inf := C.VkDescriptorSetAllocateInfo{ - sType: C.VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, - descriptorPool: pool, - descriptorSetCount: C.uint32_t(count), - pSetLayouts: &layouts[0], - } - sets := make([]DescriptorSet, count) - if err := vkErr(C.vkAllocateDescriptorSets(funcs.vkAllocateDescriptorSets, d, inf, &sets[0])); err != nil { - return nil, fmt.Errorf("vulkan: vkAllocateDescriptorSets: %w", err) - } - return sets, nil -} - -func CreateComputePipeline(d Device, mod ShaderModule, layout PipelineLayout) (Pipeline, error) { - main := C.CString("main") - defer C.free(unsafe.Pointer(main)) - inf := C.VkComputePipelineCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, - stage: C.VkPipelineShaderStageCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, - stage: C.VK_SHADER_STAGE_COMPUTE_BIT, - module: mod, - pName: main, - }, - layout: layout, - } - var pipe C.VkPipeline - if err := vkErr(C.vkCreateComputePipelines(funcs.vkCreateComputePipelines, d, nilPipelineCache, 1, &inf, nil, &pipe)); err != nil { - return nilPipeline, fmt.Errorf("vulkan: vkCreateComputePipelines: %w", err) - } - return pipe, nil -} - -func CreateFence(d Device, flags int) (Fence, error) { - inf := C.VkFenceCreateInfo{ - sType: C.VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, - flags: C.VkFenceCreateFlags(flags), - } - var f C.VkFence - if err := vkErr(C.vkCreateFence(funcs.vkCreateFence, d, &inf, nil, &f)); err != nil { - return nilFence, fmt.Errorf("vulkan: vkCreateFence: %w", err) - } - return f, nil -} - -func DestroyFence(d Device, f Fence) { - C.vkDestroyFence(funcs.vkDestroyFence, d, f, nil) -} - -func WaitForFences(d Device, fences ...Fence) error { - if len(fences) == 0 { - return nil - } - err := vkErr(C.vkWaitForFences(funcs.vkWaitForFences, d, C.uint32_t(len(fences)), &fences[0], C.VK_TRUE, 0xffffffffffffffff)) - if err != nil { - return fmt.Errorf("vulkan: vkWaitForFences: %w", err) - } - return nil -} - -func ResetFences(d Device, fences ...Fence) error { - if len(fences) == 0 { - return nil - } - err := vkErr(C.vkResetFences(funcs.vkResetFences, d, C.uint32_t(len(fences)), &fences[0])) - if err != nil { - return fmt.Errorf("vulkan: vkResetFences: %w", err) - } - return nil -} - -func BuildSubpassDependency(srcStage, dstStage PipelineStageFlags, srcMask, dstMask AccessFlags, flags DependencyFlags) SubpassDependency { - return C.VkSubpassDependency{ - srcSubpass: C.VK_SUBPASS_EXTERNAL, - srcStageMask: srcStage, - srcAccessMask: srcMask, - dstSubpass: 0, - dstStageMask: dstStage, - dstAccessMask: dstMask, - dependencyFlags: flags, - } -} - -func BuildPushConstantRange(stages ShaderStageFlags, offset, size int) PushConstantRange { - return C.VkPushConstantRange{ - stageFlags: stages, - offset: C.uint32_t(offset), - size: C.uint32_t(size), - } -} - -func BuildDescriptorPoolSize(typ DescriptorType, count int) DescriptorPoolSize { - return C.VkDescriptorPoolSize{ - _type: typ, - descriptorCount: C.uint32_t(count), - } -} - -func BuildWriteDescriptorSetImage(set DescriptorSet, binding int, typ DescriptorType, sampler Sampler, view ImageView, layout ImageLayout) WriteDescriptorSet { - return C.VkWriteDescriptorSet{ - sType: C.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - dstSet: set, - dstBinding: C.uint32_t(binding), - descriptorCount: 1, - descriptorType: typ, - pImageInfo: &C.VkDescriptorImageInfo{ - sampler: sampler, - imageView: view, - imageLayout: layout, - }, - } -} - -func BuildWriteDescriptorSetBuffer(set DescriptorSet, binding int, typ DescriptorType, buf Buffer) WriteDescriptorSet { - return C.VkWriteDescriptorSet{ - sType: C.VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, - dstSet: set, - dstBinding: C.uint32_t(binding), - descriptorCount: 1, - descriptorType: typ, - pBufferInfo: &C.VkDescriptorBufferInfo{ - buffer: buf, - _range: C.VK_WHOLE_SIZE, - }, - } -} - -func PushConstantRangeStageFlags(r PushConstantRange) ShaderStageFlags { - return r.stageFlags -} - -func PushConstantRangeOffset(r PushConstantRange) int { - return int(r.offset) -} - -func PushConstantRangeSize(r PushConstantRange) int { - return int(r.size) -} - -func QueueFamilyPropertiesFlags(p QueueFamilyProperties) QueueFlags { - return p.queueFlags -} - -func SurfaceCapabilitiesMinExtent(c SurfaceCapabilities) image.Point { - return image.Pt(int(c.minImageExtent.width), int(c.minImageExtent.height)) -} - -func SurfaceCapabilitiesMaxExtent(c SurfaceCapabilities) image.Point { - return image.Pt(int(c.maxImageExtent.width), int(c.maxImageExtent.height)) -} - -func BuildViewport(x, y, width, height float32) Viewport { - return C.VkViewport{ - x: C.float(x), - y: C.float(y), - width: C.float(width), - height: C.float(height), - maxDepth: 1.0, - } -} - -func BuildImageMemoryBarrier(img Image, srcMask, dstMask AccessFlags, oldLayout, newLayout ImageLayout, baseMip, numMips int) ImageMemoryBarrier { - return C.VkImageMemoryBarrier{ - sType: C.VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, - srcAccessMask: srcMask, - dstAccessMask: dstMask, - oldLayout: oldLayout, - newLayout: newLayout, - image: img, - subresourceRange: C.VkImageSubresourceRange{ - aspectMask: C.VK_IMAGE_ASPECT_COLOR_BIT, - baseMipLevel: C.uint32_t(baseMip), - levelCount: C.uint32_t(numMips), - layerCount: C.VK_REMAINING_ARRAY_LAYERS, - }, - } -} - -func BuildBufferMemoryBarrier(buf Buffer, srcMask, dstMask AccessFlags) BufferMemoryBarrier { - return C.VkBufferMemoryBarrier{ - sType: C.VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, - srcAccessMask: srcMask, - dstAccessMask: dstMask, - buffer: buf, - size: C.VK_WHOLE_SIZE, - } -} - -func BuildMemoryBarrier(srcMask, dstMask AccessFlags) MemoryBarrier { - return C.VkMemoryBarrier{ - sType: C.VK_STRUCTURE_TYPE_MEMORY_BARRIER, - srcAccessMask: srcMask, - dstAccessMask: dstMask, - } -} - -func BuildBufferImageCopy(bufOff, bufStride, x, y, width, height int) BufferImageCopy { - return C.VkBufferImageCopy{ - bufferOffset: C.VkDeviceSize(bufOff), - bufferRowLength: C.uint32_t(bufStride), - imageSubresource: C.VkImageSubresourceLayers{ - aspectMask: C.VK_IMAGE_ASPECT_COLOR_BIT, - layerCount: 1, - }, - imageOffset: C.VkOffset3D{ - x: C.int32_t(x), y: C.int32_t(y), z: 0, - }, - imageExtent: C.VkExtent3D{ - width: C.uint32_t(width), height: C.uint32_t(height), depth: 1, - }, - } -} - -func BuildImageCopy(srcX, srcY, dstX, dstY, width, height int) ImageCopy { - return C.VkImageCopy{ - srcSubresource: C.VkImageSubresourceLayers{ - aspectMask: C.VK_IMAGE_ASPECT_COLOR_BIT, - layerCount: 1, - }, - srcOffset: C.VkOffset3D{ - x: C.int32_t(srcX), - y: C.int32_t(srcY), - }, - dstSubresource: C.VkImageSubresourceLayers{ - aspectMask: C.VK_IMAGE_ASPECT_COLOR_BIT, - layerCount: 1, - }, - dstOffset: C.VkOffset3D{ - x: C.int32_t(dstX), - y: C.int32_t(dstY), - }, - extent: C.VkExtent3D{ - width: C.uint32_t(width), - height: C.uint32_t(height), - depth: 1, - }, - } -} - -func BuildImageBlit(srcX, srcY, dstX, dstY, srcWidth, srcHeight, dstWidth, dstHeight, srcMip, dstMip int) ImageBlit { - return C.VkImageBlit{ - srcOffsets: [2]C.VkOffset3D{ - {C.int32_t(srcX), C.int32_t(srcY), 0}, - {C.int32_t(srcWidth), C.int32_t(srcHeight), 1}, - }, - srcSubresource: C.VkImageSubresourceLayers{ - aspectMask: C.VK_IMAGE_ASPECT_COLOR_BIT, - layerCount: 1, - mipLevel: C.uint32_t(srcMip), - }, - dstOffsets: [2]C.VkOffset3D{ - {C.int32_t(dstX), C.int32_t(dstY), 0}, - {C.int32_t(dstWidth), C.int32_t(dstHeight), 1}, - }, - dstSubresource: C.VkImageSubresourceLayers{ - aspectMask: C.VK_IMAGE_ASPECT_COLOR_BIT, - layerCount: 1, - mipLevel: C.uint32_t(dstMip), - }, - } -} - -func findMemoryTypeIndex(pd C.VkPhysicalDevice, constraints C.uint32_t, wantProps C.VkMemoryPropertyFlags) (int, bool) { - var memProps C.VkPhysicalDeviceMemoryProperties - C.vkGetPhysicalDeviceMemoryProperties(funcs.vkGetPhysicalDeviceMemoryProperties, pd, &memProps) - - for i := 0; i < int(memProps.memoryTypeCount); i++ { - if (constraints & (1 << i)) == 0 { - continue - } - if (memProps.memoryTypes[i].propertyFlags & wantProps) != wantProps { - continue - } - return i, true - } - - return 0, false -} - -func choosePresentMode(pd C.VkPhysicalDevice, surf Surface) (C.VkPresentModeKHR, bool, error) { - var count C.uint32_t - err := vkErr(C.vkGetPhysicalDeviceSurfacePresentModesKHR(funcs.vkGetPhysicalDeviceSurfacePresentModesKHR, pd, surf, &count, nil)) - if err != nil { - return 0, false, fmt.Errorf("vulkan: vkGetPhysicalDeviceSurfacePresentModesKHR: %w", err) - } - if count == 0 { - return 0, false, nil - } - modes := make([]C.VkPresentModeKHR, count) - err = vkErr(C.vkGetPhysicalDeviceSurfacePresentModesKHR(funcs.vkGetPhysicalDeviceSurfacePresentModesKHR, pd, surf, &count, &modes[0])) - if err != nil { - return 0, false, fmt.Errorf("vulkan: kGetPhysicalDeviceSurfacePresentModesKHR: %w", err) - } - for _, m := range modes { - if m == C.VK_PRESENT_MODE_MAILBOX_KHR || m == C.VK_PRESENT_MODE_FIFO_KHR { - return m, true, nil - } - } - return 0, false, nil -} - -func chooseFormat(pd C.VkPhysicalDevice, surf Surface) (C.VkSurfaceFormatKHR, bool, error) { - var count C.uint32_t - err := vkErr(C.vkGetPhysicalDeviceSurfaceFormatsKHR(funcs.vkGetPhysicalDeviceSurfaceFormatsKHR, pd, surf, &count, nil)) - if err != nil { - return C.VkSurfaceFormatKHR{}, false, fmt.Errorf("vulkan: vkGetPhysicalDeviceSurfaceFormatsKHR: %w", err) - } - if count == 0 { - return C.VkSurfaceFormatKHR{}, false, nil - } - formats := make([]C.VkSurfaceFormatKHR, count) - err = vkErr(C.vkGetPhysicalDeviceSurfaceFormatsKHR(funcs.vkGetPhysicalDeviceSurfaceFormatsKHR, pd, surf, &count, &formats[0])) - if err != nil { - return C.VkSurfaceFormatKHR{}, false, fmt.Errorf("vulkan: vkGetPhysicalDeviceSurfaceFormatsKHR: %w", err) - } - // Query for format with sRGB support. - // TODO: Support devices without sRGB. - for _, f := range formats { - if f.colorSpace != C.VK_COLOR_SPACE_SRGB_NONLINEAR_KHR { - continue - } - switch f.format { - case C.VK_FORMAT_B8G8R8A8_SRGB, C.VK_FORMAT_R8G8B8A8_SRGB: - return f, true, nil - } - } - return C.VkSurfaceFormatKHR{}, false, nil -} - -func chooseQueue(pd C.VkPhysicalDevice, surf Surface, flags C.VkQueueFlags) (int, bool, error) { - queues := GetPhysicalDeviceQueueFamilyProperties(pd) - for i, q := range queues { - // Check for presentation and feature support. - if q.queueFlags&flags != flags { - continue - } - if surf != nilSurface { - // Check for presentation support. It is possible that a device has no - // queue with both rendering and presentation support, but not in reality. - // See https://github.com/KhronosGroup/Vulkan-Docs/issues/1234. - var support C.VkBool32 - if err := vkErr(C.vkGetPhysicalDeviceSurfaceSupportKHR(funcs.vkGetPhysicalDeviceSurfaceSupportKHR, pd, C.uint32_t(i), surf, &support)); err != nil { - return 0, false, fmt.Errorf("vulkan: vkGetPhysicalDeviceSurfaceSupportKHR: %w", err) - } - if support != C.VK_TRUE { - continue - } - } - return i, true, nil - } - return 0, false, nil -} - -func dlsym(handle unsafe.Pointer, s string) unsafe.Pointer { - cs := C.CString(s) - defer C.free(unsafe.Pointer(cs)) - return C.dlsym(handle, cs) -} - -func dlopen(lib string) unsafe.Pointer { - clib := C.CString(lib) - defer C.free(unsafe.Pointer(clib)) - return C.dlopen(clib, C.RTLD_NOW|C.RTLD_LOCAL) -} - -func vkErr(res C.VkResult) error { - switch res { - case C.VK_SUCCESS: - return nil - default: - return Error(res) - } -} - -func (e Error) Error() string { - return fmt.Sprintf("error %d", e) -} diff --git a/gio/internal/vk/vulkan_android.go b/gio/internal/vk/vulkan_android.go deleted file mode 100644 index cc9d8d2..0000000 --- a/gio/internal/vk/vulkan_android.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build !nowayland -// +build !nowayland - -package vk - -/* -#define VK_USE_PLATFORM_ANDROID_KHR -#define VK_NO_PROTOTYPES 1 -#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; -#include -#include - -static VkResult vkCreateAndroidSurfaceKHR(PFN_vkCreateAndroidSurfaceKHR f, VkInstance instance, const VkAndroidSurfaceCreateInfoKHR *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSurfaceKHR *pSurface) { - return f(instance, pCreateInfo, pAllocator, pSurface); -} -*/ -import "C" - -import ( - "fmt" - "unsafe" -) - -var wlFuncs struct { - vkCreateAndroidSurfaceKHR C.PFN_vkCreateAndroidSurfaceKHR -} - -func init() { - loadFuncs = append(loadFuncs, func(dlopen func(name string) *[0]byte) { - wlFuncs.vkCreateAndroidSurfaceKHR = dlopen("vkCreateAndroidSurfaceKHR") - }) -} - -func CreateAndroidSurface(inst Instance, window unsafe.Pointer) (Surface, error) { - inf := C.VkAndroidSurfaceCreateInfoKHR{ - sType: C.VK_STRUCTURE_TYPE_ANDROID_SURFACE_CREATE_INFO_KHR, - window: (*C.ANativeWindow)(window), - } - var surf Surface - if err := vkErr(C.vkCreateAndroidSurfaceKHR(wlFuncs.vkCreateAndroidSurfaceKHR, inst, &inf, nil, &surf)); err != nil { - return 0, fmt.Errorf("vulkan: vkCreateAndroidSurfaceKHR: %w", err) - } - return surf, nil -} diff --git a/gio/internal/vk/vulkan_wayland.go b/gio/internal/vk/vulkan_wayland.go deleted file mode 100644 index 798878e..0000000 --- a/gio/internal/vk/vulkan_wayland.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build ((linux && !android) || freebsd) && !nowayland -// +build linux,!android freebsd -// +build !nowayland - -package vk - -/* -#cgo linux pkg-config: wayland-client - -#define VK_USE_PLATFORM_WAYLAND_KHR -#define VK_NO_PROTOTYPES 1 -#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; -#include - -static VkResult vkCreateWaylandSurfaceKHR(PFN_vkCreateWaylandSurfaceKHR f, VkInstance instance, const VkWaylandSurfaceCreateInfoKHR *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSurfaceKHR *pSurface) { - return f(instance, pCreateInfo, pAllocator, pSurface); -} -*/ -import "C" - -import ( - "fmt" - "unsafe" -) - -var wlFuncs struct { - vkCreateWaylandSurfaceKHR C.PFN_vkCreateWaylandSurfaceKHR -} - -func init() { - loadFuncs = append(loadFuncs, func(dlopen func(name string) *[0]byte) { - wlFuncs.vkCreateWaylandSurfaceKHR = dlopen("vkCreateWaylandSurfaceKHR") - }) -} - -func CreateWaylandSurface(inst Instance, disp unsafe.Pointer, wlSurf unsafe.Pointer) (Surface, error) { - inf := C.VkWaylandSurfaceCreateInfoKHR{ - sType: C.VK_STRUCTURE_TYPE_WAYLAND_SURFACE_CREATE_INFO_KHR, - display: (*C.struct_wl_display)(disp), - surface: (*C.struct_wl_surface)(wlSurf), - } - var surf Surface - if err := vkErr(C.vkCreateWaylandSurfaceKHR(wlFuncs.vkCreateWaylandSurfaceKHR, inst, &inf, nil, &surf)); err != nil { - return 0, fmt.Errorf("vulkan: vkCreateWaylandSurfaceKHR: %w", err) - } - return surf, nil -} diff --git a/gio/internal/vk/vulkan_x11.go b/gio/internal/vk/vulkan_x11.go deleted file mode 100644 index fe6bcfc..0000000 --- a/gio/internal/vk/vulkan_x11.go +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build ((linux && !android) || freebsd) && !nox11 -// +build linux,!android freebsd -// +build !nox11 - -package vk - -/* -#define VK_USE_PLATFORM_XLIB_KHR -#define VK_NO_PROTOTYPES 1 -#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; -#include - -static VkResult vkCreateXlibSurfaceKHR(PFN_vkCreateXlibSurfaceKHR f, VkInstance instance, const VkXlibSurfaceCreateInfoKHR *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkSurfaceKHR *pSurface) { - return f(instance, pCreateInfo, pAllocator, pSurface); -} -*/ -import "C" - -import ( - "fmt" - "unsafe" -) - -var x11Funcs struct { - vkCreateXlibSurfaceKHR C.PFN_vkCreateXlibSurfaceKHR -} - -func init() { - loadFuncs = append(loadFuncs, func(dlopen func(name string) *[0]byte) { - x11Funcs.vkCreateXlibSurfaceKHR = dlopen("vkCreateXlibSurfaceKHR") - }) -} - -func CreateXlibSurface(inst Instance, dpy unsafe.Pointer, window uintptr) (Surface, error) { - inf := C.VkXlibSurfaceCreateInfoKHR{ - sType: C.VK_STRUCTURE_TYPE_XLIB_SURFACE_CREATE_INFO_KHR, - dpy: (*C.Display)(dpy), - window: (C.Window)(window), - } - var surf Surface - if err := vkErr(C.vkCreateXlibSurfaceKHR(x11Funcs.vkCreateXlibSurfaceKHR, inst, &inf, nil, &surf)); err != nil { - return 0, fmt.Errorf("vulkan: vkCreateXlibSurfaceKHR: %w", err) - } - return surf, nil -} diff --git a/gio/io/clipboard/clipboard.go b/gio/io/clipboard/clipboard.go deleted file mode 100644 index d8780f4..0000000 --- a/gio/io/clipboard/clipboard.go +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package clipboard - -import ( - "io" - - "github.com/p9c/p9/pkg/gel/gio/io/event" -) - -// WriteCmd copies Text to the clipboard. -type WriteCmd struct { - Type string - Data io.ReadCloser -} - -// ReadCmd requests the text of the clipboard, delivered to -// the handler through an [io/transfer.DataEvent]. -type ReadCmd struct { - Tag event.Tag -} - -func (WriteCmd) ImplementsCommand() {} -func (ReadCmd) ImplementsCommand() {} diff --git a/gio/io/event/event.go b/gio/io/event/event.go deleted file mode 100644 index fa6da94..0000000 --- a/gio/io/event/event.go +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package event contains types for event handling. -package event - -import ( - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Tag is the stable identifier for an event handler. -// For a handler h, the tag is typically &h. -type Tag any - -// Event is the marker interface for events. -type Event interface { - ImplementsEvent() -} - -// Filter represents a filter for [Event] types. -type Filter interface { - ImplementsFilter() -} - -// Op declares a tag for input routing at the current transformation -// and clip area hierarchy. It panics if tag is nil. -func Op(o *op.Ops, tag Tag) { - if tag == nil { - panic("Tag must be non-nil") - } - data := ops.Write1(&o.Internal, ops.TypeInputLen, tag) - data[0] = byte(ops.TypeInput) -} diff --git a/gio/io/input/clipboard.go b/gio/io/input/clipboard.go deleted file mode 100644 index 34a5622..0000000 --- a/gio/io/input/clipboard.go +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package input - -import ( - "io" - "slices" - - "github.com/p9c/p9/pkg/gel/gio/io/clipboard" - "github.com/p9c/p9/pkg/gel/gio/io/event" -) - -// clipboardState contains the state for clipboard event routing. -type clipboardState struct { - receivers []event.Tag -} - -type clipboardQueue struct { - // request avoid read clipboard every frame while waiting. - requested bool - mime string - text []byte -} - -// WriteClipboard returns the most recent data to be copied -// to the clipboard, if any. -func (q *clipboardQueue) WriteClipboard() (mime string, content []byte, ok bool) { - if q.text == nil { - return "", nil, false - } - content = q.text - q.text = nil - return q.mime, content, true -} - -// ClipboardRequested reports if any new handler is waiting -// to read the clipboard. -func (q *clipboardQueue) ClipboardRequested(state clipboardState) bool { - req := len(state.receivers) > 0 && q.requested - q.requested = false - return req -} - -func (q *clipboardQueue) Push(state clipboardState, e event.Event) (clipboardState, []taggedEvent) { - var evts []taggedEvent - for _, r := range state.receivers { - evts = append(evts, taggedEvent{tag: r, event: e}) - } - state.receivers = nil - return state, evts -} - -func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) { - defer req.Data.Close() - content, err := io.ReadAll(req.Data) - if err != nil { - return - } - q.mime = req.Type - q.text = content -} - -func (q *clipboardQueue) ProcessReadClipboard(state clipboardState, tag event.Tag) clipboardState { - if slices.Contains(state.receivers, tag) { - return state - } - n := len(state.receivers) - state.receivers = append(state.receivers[:n:n], tag) - q.requested = true - return state -} diff --git a/gio/io/input/clipboard_test.go b/gio/io/input/clipboard_test.go deleted file mode 100644 index 8e6a7a3..0000000 --- a/gio/io/input/clipboard_test.go +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package input - -import ( - "io" - "strings" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/io/clipboard" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -func TestClipboardDuplicateEvent(t *testing.T) { - ops, r, handlers := new(op.Ops), new(Router), make([]int, 2) - - // Both must receive the event once. - r.Source().Execute(clipboard.ReadCmd{Tag: &handlers[0]}) - r.Source().Execute(clipboard.ReadCmd{Tag: &handlers[1]}) - - event := transfer.DataEvent{ - Type: "application/text", - Open: func() io.ReadCloser { - return io.NopCloser(strings.NewReader("Test")) - }, - } - r.Queue(event) - for i := range handlers { - f := transfer.TargetFilter{Target: &handlers[i], Type: "application/text"} - assertEventTypeSequence(t, events(r, -1, f), transfer.DataEvent{}) - } - assertClipboardReadCmd(t, r, 0) - - r.Source().Execute(clipboard.ReadCmd{Tag: &handlers[0]}) - - r.Frame(ops) - // No ClipboardEvent sent - assertClipboardReadCmd(t, r, 1) - for i := range handlers { - f := transfer.TargetFilter{Target: &handlers[i]} - assertEventTypeSequence(t, events(r, -1, f)) - } -} - -func TestQueueProcessReadClipboard(t *testing.T) { - ops, r, handler := new(op.Ops), new(Router), make([]int, 2) - - // Request read - r.Source().Execute(clipboard.ReadCmd{Tag: &handler[0]}) - - assertClipboardReadCmd(t, r, 1) - ops.Reset() - - for range 3 { - // No ReadCmd - // One receiver must still wait for response - - r.Frame(ops) - assertClipboardReadDuplicated(t, r, 1) - } - - // Send the clipboard event - event := transfer.DataEvent{ - Type: "application/text", - Open: func() io.ReadCloser { - return io.NopCloser(strings.NewReader("Text 2")) - }, - } - r.Queue(event) - assertEventTypeSequence(t, events(r, -1, transfer.TargetFilter{Target: &handler[0], Type: "application/text"}), transfer.DataEvent{}) - assertClipboardReadCmd(t, r, 0) -} - -func TestQueueProcessWriteClipboard(t *testing.T) { - r := new(Router) - - const mime = "application/text" - r.Source().Execute(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 1"))}) - - assertClipboardWriteCmd(t, r, mime, "Write 1") - assertClipboardWriteCmd(t, r, "", "") - - r.Source().Execute(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 2"))}) - - assertClipboardReadCmd(t, r, 0) - assertClipboardWriteCmd(t, r, mime, "Write 2") -} - -func assertClipboardReadCmd(t *testing.T, router *Router, expected int) { - t.Helper() - if got := len(router.state().receivers); got != expected { - t.Errorf("unexpected %d receivers, got %d", expected, got) - } - if router.ClipboardRequested() != (expected > 0) { - t.Error("missing requests") - } -} - -func assertClipboardReadDuplicated(t *testing.T, router *Router, expected int) { - t.Helper() - if len(router.state().receivers) != expected { - t.Error("receivers removed") - } - if router.ClipboardRequested() != false { - t.Error("duplicated requests") - } -} - -func assertClipboardWriteCmd(t *testing.T, router *Router, mimeExp, expected string) { - t.Helper() - if (router.cqueue.text != nil) != (expected != "") { - t.Error("text not defined") - } - mime, text, ok := router.cqueue.WriteClipboard() - if ok != (expected != "") { - t.Error("duplicated requests") - } - if string(mime) != mimeExp { - t.Errorf("got MIME type %s, expected %s", mime, mimeExp) - } - if string(text) != expected { - t.Errorf("got text %s, expected %s", text, expected) - } -} diff --git a/gio/io/input/doc.go b/gio/io/input/doc.go deleted file mode 100644 index c029576..0000000 --- a/gio/io/input/doc.go +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package input implements input routing and tracking of interface -state for a window. - -The [Source] is the interface between the window and the widgets -of a user interface and is exposed by [gioui.org/app.FrameEvent] -received from windows. - -The [Router] is used by [gioui.org/app.Window] to track window state and route -events from the platform to event handlers. It is otherwise only -useful for using Gio with external window implementations. -*/ -package input diff --git a/gio/io/input/key.go b/gio/io/input/key.go deleted file mode 100644 index 000fc7b..0000000 --- a/gio/io/input/key.go +++ /dev/null @@ -1,364 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package input - -import ( - "image" - "slices" - "sort" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" -) - -// EditorState represents the state of an editor needed by input handlers. -type EditorState struct { - Selection struct { - Transform f32.Affine2D - key.Range - key.Caret - } - Snippet key.Snippet -} - -type TextInputState uint8 - -type keyQueue struct { - order []event.Tag - dirOrder []dirFocusEntry - hint key.InputHint -} - -// keyState is the input state related to key events. -type keyState struct { - focus event.Tag - state TextInputState - content EditorState -} - -type keyHandler struct { - // visible will be true if the InputOp is present - // in the current frame. - visible bool - // reset tracks whether the handler has seen a - // focus reset. - reset bool - hint key.InputHint - orderPlusOne int - dirOrder int - trans f32.Affine2D -} - -type keyFilter []key.Filter - -type dirFocusEntry struct { - tag event.Tag - row int - area int - bounds image.Rectangle -} - -const ( - TextInputKeep TextInputState = iota - TextInputClose - TextInputOpen -) - -func (k *keyHandler) inputHint(hint key.InputHint) { - k.hint = hint -} - -// InputState returns the input state and returns a state -// reset to [TextInputKeep]. -func (s keyState) InputState() (keyState, TextInputState) { - state := s.state - s.state = TextInputKeep - return s, state -} - -// InputHint returns the input hint from the focused handler and whether it was -// changed since the last call. -func (q *keyQueue) InputHint(handlers map[event.Tag]*handler, state keyState) (key.InputHint, bool) { - focused, ok := handlers[state.focus] - if !ok { - return q.hint, false - } - old := q.hint - q.hint = focused.key.hint - return q.hint, old != q.hint -} - -func (k *keyHandler) Reset() { - k.visible = false - k.orderPlusOne = 0 - k.hint = key.HintAny -} - -func (q *keyQueue) Reset() { - q.order = q.order[:0] - q.dirOrder = q.dirOrder[:0] -} - -func (k *keyHandler) ResetEvent() (event.Event, bool) { - if k.reset { - return nil, false - } - k.reset = true - return key.FocusEvent{Focus: false}, true -} - -func (q *keyQueue) Frame(handlers map[event.Tag]*handler, state keyState) keyState { - if state.focus != nil { - if h, ok := handlers[state.focus]; !ok || !h.filter.focusable || !h.key.visible { - // Remove focus from the handler that is no longer focusable. - state.focus = nil - state.state = TextInputClose - } - } - q.updateFocusLayout(handlers) - return state -} - -// updateFocusLayout partitions input handlers handlers into rows -// for directional focus moves. -// -// The approach is greedy: pick the topmost handler and create a row -// containing it. Then, extend the handler bounds to a horizontal beam -// and add to the row every handler whose center intersect it. Repeat -// until no handlers remain. -func (q *keyQueue) updateFocusLayout(handlers map[event.Tag]*handler) { - order := q.dirOrder - // Sort by ascending y position. - sort.SliceStable(order, func(i, j int) bool { - return order[i].bounds.Min.Y < order[j].bounds.Min.Y - }) - row := 0 - for len(order) > 0 { - h := &order[0] - h.row = row - bottom := h.bounds.Max.Y - end := 1 - for ; end < len(order); end++ { - h := &order[end] - center := (h.bounds.Min.Y + h.bounds.Max.Y) / 2 - if center > bottom { - break - } - h.row = row - } - // Sort row by ascending x position. - sort.SliceStable(order[:end], func(i, j int) bool { - return order[i].bounds.Min.X < order[j].bounds.Min.X - }) - order = order[end:] - row++ - } - for i, o := range q.dirOrder { - handlers[o.tag].key.dirOrder = i - } -} - -// MoveFocus attempts to move the focus in the direction of dir. -func (q *keyQueue) MoveFocus(handlers map[event.Tag]*handler, state keyState, dir key.FocusDirection) (keyState, []taggedEvent) { - if len(q.dirOrder) == 0 { - return state, nil - } - order := 0 - if state.focus != nil { - order = handlers[state.focus].key.dirOrder - } - focus := q.dirOrder[order] - switch dir { - case key.FocusForward, key.FocusBackward: - if len(q.order) == 0 { - break - } - order := 0 - if dir == key.FocusBackward { - order = -1 - } - if state.focus != nil { - order = handlers[state.focus].key.orderPlusOne - 1 - if dir == key.FocusForward { - order++ - } else { - order-- - } - } - order = (order + len(q.order)) % len(q.order) - return q.Focus(handlers, state, q.order[order]) - case key.FocusRight, key.FocusLeft: - next := order - if state.focus != nil { - next = order + 1 - if dir == key.FocusLeft { - next = order - 1 - } - } - if 0 <= next && next < len(q.dirOrder) { - newFocus := q.dirOrder[next] - if newFocus.row == focus.row { - return q.Focus(handlers, state, newFocus.tag) - } - } - case key.FocusUp, key.FocusDown: - delta := +1 - if dir == key.FocusUp { - delta = -1 - } - nextRow := 0 - if state.focus != nil { - nextRow = focus.row + delta - } - var closest event.Tag - dist := int(1e6) - center := (focus.bounds.Min.X + focus.bounds.Max.X) / 2 - loop: - for 0 <= order && order < len(q.dirOrder) { - next := q.dirOrder[order] - switch next.row { - case nextRow: - nextCenter := (next.bounds.Min.X + next.bounds.Max.X) / 2 - d := center - nextCenter - if d < 0 { - d = -d - } - if d > dist { - break loop - } - dist = d - closest = next.tag - case nextRow + delta: - break loop - } - order += delta - } - if closest != nil { - return q.Focus(handlers, state, closest) - } - } - return state, nil -} - -func (q *keyQueue) BoundsFor(k *keyHandler) image.Rectangle { - order := k.dirOrder - return q.dirOrder[order].bounds -} - -func (q *keyQueue) AreaFor(k *keyHandler) int { - order := k.dirOrder - return q.dirOrder[order].area -} - -func (k *keyFilter) Matches(focus event.Tag, e key.Event, system bool) bool { - for _, f := range *k { - if keyFilterMatch(focus, f, e, system) { - return true - } - } - return false -} - -func keyFilterMatch(focus event.Tag, f key.Filter, e key.Event, system bool) bool { - if f.Focus != nil && f.Focus != focus { - return false - } - if (f.Name != "" || system) && f.Name != e.Name { - return false - } - if e.Modifiers&f.Required != f.Required { - return false - } - if e.Modifiers&^(f.Required|f.Optional) != 0 { - return false - } - return true -} - -func (q *keyQueue) Focus(handlers map[event.Tag]*handler, state keyState, focus event.Tag) (keyState, []taggedEvent) { - if focus == state.focus { - return state, nil - } - state.content = EditorState{} - state.content.Selection.Transform = f32.AffineId() - var evts []taggedEvent - if state.focus != nil { - evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: false}}) - } - state.focus = focus - if state.focus != nil { - evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: true}}) - } - if state.focus == nil || state.state == TextInputKeep { - state.state = TextInputClose - } - return state, evts -} - -func (s keyState) softKeyboard(show bool) keyState { - if show { - s.state = TextInputOpen - } else { - s.state = TextInputClose - } - return s -} - -func (k *keyFilter) Add(f key.Filter) { - if slices.Contains(*k, f) { - return - } - *k = append(*k, f) -} - -func (k *keyFilter) Merge(k2 keyFilter) { - *k = append(*k, k2...) -} - -func (q *keyQueue) inputOp(tag event.Tag, state *keyHandler, t f32.Affine2D, area int, bounds image.Rectangle) { - state.visible = true - if state.orderPlusOne == 0 { - state.orderPlusOne = len(q.order) + 1 - q.order = append(q.order, tag) - q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds}) - } - state.trans = t -} - -func (q *keyQueue) setSelection(state keyState, req key.SelectionCmd) keyState { - if req.Tag != state.focus { - return state - } - state.content.Selection.Range = req.Range - state.content.Selection.Caret = req.Caret - return state -} - -func (q *keyQueue) editorState(handlers map[event.Tag]*handler, state keyState) EditorState { - s := state.content - if f := state.focus; f != nil { - s.Selection.Transform = handlers[f].key.trans - } - return s -} - -func (q *keyQueue) setSnippet(state keyState, req key.SnippetCmd) keyState { - if req.Tag == state.focus { - state.content.Snippet = req.Snippet - } - return state -} - -func (t TextInputState) String() string { - switch t { - case TextInputKeep: - return "Keep" - case TextInputClose: - return "Close" - case TextInputOpen: - return "Open" - default: - panic("unexpected value") - } -} diff --git a/gio/io/input/key_test.go b/gio/io/input/key_test.go deleted file mode 100644 index 0fe633d..0000000 --- a/gio/io/input/key_test.go +++ /dev/null @@ -1,332 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package input - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -func TestAllMatchKeyFilter(t *testing.T) { - r := new(Router) - r.Event(key.Filter{}) - ke := key.Event{Name: "A"} - r.Queue(ke) - // Catch-all gets all non-system events. - assertEventSequence(t, events(r, -1, key.Filter{}), ke) - - r = new(Router) - r.Event(key.Filter{Name: "A"}) - r.Queue(SystemEvent{ke}) - if _, handled := r.WakeupTime(); !handled { - t.Errorf("system event was unexpectedly ignored") - } - // Only specific filters match system events. - assertEventSequence(t, events(r, -1, key.Filter{Name: "A"}), ke) -} - -func TestInputHint(t *testing.T) { - r := new(Router) - if hint, changed := r.TextInputHint(); hint != key.HintAny || changed { - t.Fatal("unexpected hint") - } - ops := new(op.Ops) - h := new(int) - key.InputHintOp{Tag: h, Hint: key.HintEmail}.Add(ops) - r.Frame(ops) - if hint, changed := r.TextInputHint(); hint != key.HintAny || changed { - t.Fatal("unexpected hint") - } - r.Source().Execute(key.FocusCmd{Tag: h}) - if hint, changed := r.TextInputHint(); hint != key.HintEmail || !changed { - t.Fatal("unexpected hint") - } -} - -func TestDeferred(t *testing.T) { - r := new(Router) - h := new(int) - f := []event.Filter{ - key.FocusFilter{Target: h}, - key.Filter{Name: "A"}, - } - // Provoke deferring by exhausting events for h. - events(r, -1, f...) - r.Source().Execute(key.FocusCmd{Tag: h}) - ke := key.Event{Name: "A"} - r.Queue(ke) - // All events are deferred at this point. - assertEventSequence(t, events(r, -1, f...)) - r.Frame(new(op.Ops)) - // But delivered after a frame. - assertEventSequence(t, events(r, -1, f...), key.FocusEvent{Focus: true}, ke) -} - -func TestInputWakeup(t *testing.T) { - handler := new(int) - var ops op.Ops - // InputOps shouldn't trigger redraws. - event.Op(&ops, handler) - - var r Router - // Reset events shouldn't either. - evts := events(&r, -1, key.FocusFilter{Target: new(int)}, key.Filter{Name: "A"}) - assertEventSequence(t, evts, key.FocusEvent{Focus: false}) - r.Frame(&ops) - if _, wake := r.WakeupTime(); wake { - t.Errorf("InputOp or the resetting FocusEvent triggered a wakeup") - } - // And neither does events that don't match anything. - r.Queue(key.SnippetEvent{}) - if _, handled := r.WakeupTime(); handled { - t.Errorf("a not-matching event triggered a wakeup") - } - // However, events that does match should trigger wakeup. - r.Queue(key.Event{Name: "A"}) - if _, handled := r.WakeupTime(); !handled { - t.Errorf("a key.Event didn't trigger redraw") - } -} - -func TestKeyMultiples(t *testing.T) { - handlers := make([]int, 3) - r := new(Router) - r.Source().Execute(key.SoftKeyboardCmd{Show: true}) - for i := range handlers { - assertEventSequence(t, events(r, 1, key.FocusFilter{Target: &handlers[i]}), key.FocusEvent{Focus: false}) - } - r.Source().Execute(key.FocusCmd{Tag: &handlers[2]}) - assertEventSequence(t, events(r, -1, key.FocusFilter{Target: &handlers[2]}), key.FocusEvent{Focus: true}) - assertFocus(t, r, &handlers[2]) - - assertKeyboard(t, r, TextInputOpen) -} - -func TestKeySoftKeyboardNoFocus(t *testing.T) { - r := new(Router) - - // It's possible to open the keyboard - // without any active focus: - r.Source().Execute(key.SoftKeyboardCmd{Show: true}) - - assertFocus(t, r, nil) - assertKeyboard(t, r, TextInputOpen) -} - -func TestKeyRemoveFocus(t *testing.T) { - handlers := make([]int, 2) - r := new(Router) - - filters := func(h event.Tag) []event.Filter { - return []event.Filter{ - key.FocusFilter{Target: h}, - key.Filter{Focus: h, Name: key.NameTab, Required: key.ModShortcut}, - } - } - var all []event.Filter - for i := range handlers { - all = append(all, filters(&handlers[i])...) - } - assertEventSequence(t, events(r, len(handlers), all...), key.FocusEvent{}, key.FocusEvent{}) - r.Source().Execute(key.FocusCmd{Tag: &handlers[0]}) - r.Source().Execute(key.SoftKeyboardCmd{Show: true}) - - evt := key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press} - r.Queue(evt) - - assertEventSequence(t, events(r, 2, filters(&handlers[0])...), key.FocusEvent{Focus: true}, evt) - assertFocus(t, r, &handlers[0]) - assertKeyboard(t, r, TextInputOpen) - - // Frame removes focus from tags that don't filter for focus events nor mentioned in an InputOp. - r.Source().Execute(key.FocusCmd{Tag: new(int)}) - r.Frame(new(op.Ops)) - - assertEventSequence(t, events(r, -1, filters(&handlers[1])...)) - assertFocus(t, r, nil) - assertKeyboard(t, r, TextInputClose) - - // Set focus to InputOp which already - // exists in the previous frame: - r.Source().Execute(key.FocusCmd{Tag: &handlers[0]}) - assertFocus(t, r, &handlers[0]) -} - -func TestKeyFocusedInvisible(t *testing.T) { - handlers := make([]int, 2) - ops := new(op.Ops) - r := new(Router) - - for i := range handlers { - assertEventSequence(t, events(r, 1, key.FocusFilter{Target: &handlers[i]}), key.FocusEvent{Focus: false}) - } - - // Set new InputOp with focus: - r.Source().Execute(key.FocusCmd{Tag: &handlers[0]}) - r.Source().Execute(key.SoftKeyboardCmd{Show: true}) - - assertEventSequence(t, events(r, 1, key.FocusFilter{Target: &handlers[0]}), key.FocusEvent{Focus: true}) - assertFocus(t, r, &handlers[0]) - assertKeyboard(t, r, TextInputOpen) - - // Frame will clear the focus because the handler is not visible. - r.Frame(ops) - - for i := range handlers { - assertEventSequence(t, events(r, -1, key.FocusFilter{Target: &handlers[i]})) - } - assertFocus(t, r, nil) - assertKeyboard(t, r, TextInputClose) - - r.Frame(ops) - r.Frame(ops) - - ops.Reset() - - // Respawn the first element: - // It must receive one `Event{Focus: false}`. - event.Op(ops, &handlers[0]) - - assertEventSequence(t, events(r, -1, key.FocusFilter{Target: &handlers[0]}), key.FocusEvent{Focus: false}) -} - -func TestNoOps(t *testing.T) { - r := new(Router) - r.Frame(nil) -} - -func TestDirectionalFocus(t *testing.T) { - ops := new(op.Ops) - r := new(Router) - handlers := []image.Rectangle{ - image.Rect(10, 10, 50, 50), - image.Rect(50, 20, 100, 80), - image.Rect(20, 26, 60, 80), - image.Rect(10, 60, 50, 100), - } - - for i, bounds := range handlers { - cl := clip.Rect(bounds).Push(ops) - event.Op(ops, &handlers[i]) - cl.Pop() - events(r, -1, key.FocusFilter{Target: &handlers[i]}) - } - r.Frame(ops) - - r.MoveFocus(key.FocusLeft) - assertFocus(t, r, &handlers[0]) - r.MoveFocus(key.FocusLeft) - assertFocus(t, r, &handlers[0]) - r.MoveFocus(key.FocusRight) - assertFocus(t, r, &handlers[1]) - r.MoveFocus(key.FocusRight) - assertFocus(t, r, &handlers[1]) - r.MoveFocus(key.FocusDown) - assertFocus(t, r, &handlers[2]) - r.MoveFocus(key.FocusDown) - assertFocus(t, r, &handlers[2]) - r.MoveFocus(key.FocusLeft) - assertFocus(t, r, &handlers[3]) - r.MoveFocus(key.FocusUp) - assertFocus(t, r, &handlers[0]) - - r.MoveFocus(key.FocusForward) - assertFocus(t, r, &handlers[1]) - r.MoveFocus(key.FocusBackward) - assertFocus(t, r, &handlers[0]) -} - -func TestFocusScroll(t *testing.T) { - ops := new(op.Ops) - r := new(Router) - h := new(int) - - filters := []event.Filter{ - key.FocusFilter{Target: h}, - pointer.Filter{ - Target: h, - Kinds: pointer.Scroll, - ScrollX: pointer.ScrollRange{Min: -100, Max: +100}, - ScrollY: pointer.ScrollRange{Min: -100, Max: +100}, - }, - } - events(r, -1, filters...) - parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops) - cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops) - event.Op(ops, h) - // Test that h is scrolled even if behind another handler. - event.Op(ops, new(int)) - cl.Pop() - parent.Pop() - r.Frame(ops) - - r.MoveFocus(key.FocusLeft) - r.RevealFocus(image.Rect(0, 0, 15, 40)) - evts := events(r, -1, filters...) - assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9)) -} - -func TestFocusClick(t *testing.T) { - ops := new(op.Ops) - r := new(Router) - h := new(int) - - filters := []event.Filter{ - key.FocusFilter{Target: h}, - pointer.Filter{ - Target: h, - Kinds: pointer.Press | pointer.Release | pointer.Cancel, - }, - } - assertEventPointerTypeSequence(t, events(r, -1, filters...), pointer.Cancel) - cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops) - event.Op(ops, h) - cl.Pop() - r.Frame(ops) - - r.MoveFocus(key.FocusLeft) - r.ClickFocus() - - assertEventPointerTypeSequence(t, events(r, -1, filters...), pointer.Press, pointer.Release) -} - -func TestNoFocus(t *testing.T) { - r := new(Router) - r.MoveFocus(key.FocusForward) -} - -func TestKeyRouting(t *testing.T) { - r := new(Router) - h := new(int) - A, B := key.Event{Name: "A"}, key.Event{Name: "B"} - // Register filters. - events(r, -1, key.Filter{Name: "A"}, key.Filter{Name: "B"}) - r.Frame(new(op.Ops)) - r.Queue(A, B) - // The handler is not focused, so only B is delivered. - assertEventSequence(t, events(r, -1, key.Filter{Focus: h, Name: "A"}, key.Filter{Name: "B"}), B) - r.Source().Execute(key.FocusCmd{Tag: h}) - // A is delivered to the focused handler. - assertEventSequence(t, events(r, -1, key.Filter{Focus: h, Name: "A"}, key.Filter{Name: "B"}), A) -} - -func assertFocus(t *testing.T, router *Router, expected event.Tag) { - t.Helper() - if !router.Source().Focused(expected) { - t.Errorf("expected %v to be focused", expected) - } -} - -func assertKeyboard(t *testing.T, router *Router, expected TextInputState) { - t.Helper() - if got := router.state().state; got != expected { - t.Errorf("expected %v keyboard, got %v", expected, got) - } -} diff --git a/gio/io/input/pointer.go b/gio/io/input/pointer.go deleted file mode 100644 index 6a92f9f..0000000 --- a/gio/io/input/pointer.go +++ /dev/null @@ -1,1008 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package input - -import ( - "image" - "io" - "slices" - - "github.com/p9c/p9/pkg/gel/gio/f32" - f32internal "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" -) - -type pointerQueue struct { - hitTree []hitNode - areas []areaNode - - semantic struct { - idsAssigned bool - lastID SemanticID - // contentIDs maps semantic content to a list of semantic IDs - // previously assigned. It is used to maintain stable IDs across - // frames. - contentIDs map[semanticContent][]semanticID - } -} - -type hitNode struct { - next int - area int - - // For handler nodes. - tag event.Tag - pass bool -} - -// pointerState is the input state related to pointer events. -type pointerState struct { - cursor pointer.Cursor - pointers []pointerInfo -} - -type pointerInfo struct { - id pointer.ID - pressed bool - handlers []event.Tag - // last tracks the last pointer event received, - // used while processing frame events. - last pointer.Event - - // entered tracks the tags that contain the pointer. - entered []event.Tag - - dataSource event.Tag // dragging source tag - dataTarget event.Tag // dragging target tag -} - -type pointerHandler struct { - // areaPlusOne is the index into the list of pointerQueue.areas, plus 1. - areaPlusOne int - // setup tracks whether the handler has received - // the pointer.Cancel event that resets its state. - setup bool -} - -// pointerFilter represents the union of a set of pointer filters. -type pointerFilter struct { - kinds pointer.Kind - // min and max horizontal/vertical scroll - scrollX, scrollY pointer.ScrollRange - - sourceMimes []string - targetMimes []string -} - -type areaOp struct { - kind areaKind - rect image.Rectangle -} - -type areaNode struct { - trans f32.Affine2D - area areaOp - - cursor pointer.Cursor - - // Tree indices, with -1 being the sentinel. - parent int - firstChild int - lastChild int - sibling int - - semantic struct { - valid bool - id SemanticID - content semanticContent - } - action system.Action -} - -type areaKind uint8 - -// collectState represents the state for pointerCollector. -type collectState struct { - t f32.Affine2D - // nodePlusOne is the current node index, plus one to - // make the zero value collectState the initial state. - nodePlusOne int - pass int -} - -// pointerCollector tracks the state needed to update an pointerQueue -// from pointer ops. -type pointerCollector struct { - q *pointerQueue - state collectState - nodeStack []int -} - -type semanticContent struct { - tag event.Tag - label string - desc string - class semantic.ClassOp - gestures SemanticGestures - selected bool - disabled bool -} - -type semanticID struct { - id SemanticID - used bool -} - -const ( - areaRect areaKind = iota - areaEllipse -) - -func (c *pointerCollector) resetState() { - c.state = collectState{ - t: f32.AffineId(), - } - c.nodeStack = c.nodeStack[:0] - // Pop every node except the root. - if len(c.q.hitTree) > 0 { - c.state.nodePlusOne = 0 + 1 - } -} - -func (c *pointerCollector) setTrans(t f32.Affine2D) { - c.state.t = t -} - -func (c *pointerCollector) clip(op ops.ClipOp) { - kind := areaRect - if op.Shape == ops.Ellipse { - kind = areaEllipse - } - c.pushArea(kind, op.Bounds) -} - -func (c *pointerCollector) pushArea(kind areaKind, bounds image.Rectangle) { - parentID := c.currentArea() - areaID := len(c.q.areas) - areaOp := areaOp{kind: kind, rect: bounds} - if parentID != -1 { - parent := &c.q.areas[parentID] - if parent.firstChild == -1 { - parent.firstChild = areaID - } - if siblingID := parent.lastChild; siblingID != -1 { - c.q.areas[siblingID].sibling = areaID - } - parent.lastChild = areaID - } - an := areaNode{ - trans: c.state.t, - area: areaOp, - parent: parentID, - sibling: -1, - firstChild: -1, - lastChild: -1, - } - - c.q.areas = append(c.q.areas, an) - c.nodeStack = append(c.nodeStack, c.state.nodePlusOne-1) - c.addHitNode(hitNode{ - area: areaID, - pass: true, - }) -} - -func (c *pointerCollector) popArea() { - n := len(c.nodeStack) - c.state.nodePlusOne = c.nodeStack[n-1] + 1 - c.nodeStack = c.nodeStack[:n-1] -} - -func (c *pointerCollector) pass() { - c.state.pass++ -} - -func (c *pointerCollector) popPass() { - c.state.pass-- -} - -func (c *pointerCollector) currentArea() int { - if i := c.state.nodePlusOne - 1; i != -1 { - n := c.q.hitTree[i] - return n.area - } - return -1 -} - -func (c *pointerCollector) currentAreaBounds() image.Rectangle { - a := c.currentArea() - if a == -1 { - panic("no root area") - } - return c.q.areas[a].bounds() -} - -func (c *pointerCollector) addHitNode(n hitNode) { - n.next = c.state.nodePlusOne - 1 - c.q.hitTree = append(c.q.hitTree, n) - c.state.nodePlusOne = len(c.q.hitTree) - 1 + 1 -} - -// newHandler returns the current handler or a new one for tag. -func (c *pointerCollector) newHandler(tag event.Tag, state *pointerHandler) { - areaID := c.currentArea() - c.addHitNode(hitNode{ - area: areaID, - tag: tag, - pass: c.state.pass > 0, - }) - state.areaPlusOne = areaID + 1 -} - -func (s *pointerHandler) Reset() { - s.areaPlusOne = 0 -} - -func (c *pointerCollector) actionInputOp(act system.Action) { - areaID := c.currentArea() - area := &c.q.areas[areaID] - area.action = act -} - -func (q *pointerQueue) grab(state pointerState, req pointer.GrabCmd) (pointerState, []taggedEvent) { - var evts []taggedEvent - for _, p := range state.pointers { - if !p.pressed || p.id != req.ID { - continue - } - // Verify that the grabber is among the handlers. - found := slices.Contains(p.handlers, req.Tag) - if !found { - continue - } - // Drop other handlers that lost their grab. - for i := len(p.handlers) - 1; i >= 0; i-- { - if tag := p.handlers[i]; tag != req.Tag { - evts = append(evts, taggedEvent{ - tag: tag, - event: pointer.Event{Kind: pointer.Cancel}, - }) - state = dropHandler(state, tag) - } - } - break - } - return state, evts -} - -func (c *pointerCollector) inputOp(tag event.Tag, state *pointerHandler) { - areaID := c.currentArea() - area := &c.q.areas[areaID] - area.semantic.content.tag = tag - c.newHandler(tag, state) -} - -func (p *pointerFilter) Add(f event.Filter) { - switch f := f.(type) { - case transfer.SourceFilter: - if slices.Contains(p.sourceMimes, f.Type) { - return - } - p.sourceMimes = append(p.sourceMimes, f.Type) - case transfer.TargetFilter: - if slices.Contains(p.targetMimes, f.Type) { - return - } - p.targetMimes = append(p.targetMimes, f.Type) - case pointer.Filter: - p.kinds = p.kinds | f.Kinds - p.scrollX = p.scrollX.Union(f.ScrollX) - p.scrollY = p.scrollY.Union(f.ScrollY) - } -} - -func (p *pointerFilter) Matches(e event.Event) bool { - switch e := e.(type) { - case pointer.Event: - return e.Kind&p.kinds == e.Kind - case transfer.CancelEvent, transfer.InitiateEvent: - return len(p.sourceMimes) > 0 || len(p.targetMimes) > 0 - case transfer.RequestEvent: - if slices.Contains(p.sourceMimes, e.Type) { - return true - } - case transfer.DataEvent: - if slices.Contains(p.targetMimes, e.Type) { - return true - } - } - return false -} - -func (p *pointerFilter) Merge(p2 pointerFilter) { - p.kinds = p.kinds | p2.kinds - p.scrollX = p.scrollX.Union(p2.scrollX) - p.scrollY = p.scrollY.Union(p2.scrollY) - p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...) - p.targetMimes = append(p.targetMimes, p2.targetMimes...) -} - -// clampScroll splits a scroll distance in the remaining scroll and the -// scroll accepted by the filter. -func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) { - left.X, scrolled.X = clampSplit(scroll.X, p.scrollX.Min, p.scrollX.Max) - left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max) - return -} - -func clampSplit(v float32, min, max int) (float32, float32) { - if m := float32(max); v > m { - return v - m, m - } - if m := float32(min); v < m { - return v - m, m - } - return 0, v -} - -func (s *pointerHandler) ResetEvent() (event.Event, bool) { - if s.setup { - return nil, false - } - s.setup = true - return pointer.Event{Kind: pointer.Cancel}, true -} - -func (c *pointerCollector) semanticLabel(lbl string) { - areaID := c.currentArea() - area := &c.q.areas[areaID] - area.semantic.valid = true - area.semantic.content.label = lbl -} - -func (c *pointerCollector) semanticDesc(desc string) { - areaID := c.currentArea() - area := &c.q.areas[areaID] - area.semantic.valid = true - area.semantic.content.desc = desc -} - -func (c *pointerCollector) semanticClass(class semantic.ClassOp) { - areaID := c.currentArea() - area := &c.q.areas[areaID] - area.semantic.valid = true - area.semantic.content.class = class -} - -func (c *pointerCollector) semanticSelected(selected bool) { - areaID := c.currentArea() - area := &c.q.areas[areaID] - area.semantic.valid = true - area.semantic.content.selected = selected -} - -func (c *pointerCollector) semanticEnabled(enabled bool) { - areaID := c.currentArea() - area := &c.q.areas[areaID] - area.semantic.valid = true - area.semantic.content.disabled = !enabled -} - -func (c *pointerCollector) cursor(cursor pointer.Cursor) { - areaID := c.currentArea() - area := &c.q.areas[areaID] - area.cursor = cursor -} - -func (q *pointerQueue) offerData(handlers map[event.Tag]*handler, state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) { - var evts []taggedEvent - for i, p := range state.pointers { - if p.dataSource != req.Tag { - continue - } - if p.dataTarget != nil { - evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{ - Type: req.Type, - Open: func() io.ReadCloser { - return req.Data - }, - }}) - } - state.pointers = slices.Clone(state.pointers) - state.pointers[i], evts = q.deliverTransferCancelEvent(handlers, p, evts) - break - } - return state, evts -} - -func (c *pointerCollector) Reset() { - c.q.reset() - c.resetState() - c.ensureRoot() -} - -// Ensure implicit root area for semantic descriptions to hang onto. -func (c *pointerCollector) ensureRoot() { - if len(c.q.areas) > 0 { - return - } - c.pushArea(areaRect, image.Rect(-1e6, -1e6, 1e6, 1e6)) - // Make it semantic to ensure a single semantic root. - c.q.areas[0].semantic.valid = true -} - -func (q *pointerQueue) assignSemIDs() { - if q.semantic.idsAssigned { - return - } - q.semantic.idsAssigned = true - for i, a := range q.areas { - if a.semantic.valid { - q.areas[i].semantic.id = q.semanticIDFor(a.semantic.content) - } - } -} - -func (q *pointerQueue) AppendSemantics(nodes []SemanticNode) []SemanticNode { - q.assignSemIDs() - nodes = q.appendSemanticChildren(nodes, 0) - nodes = q.appendSemanticArea(nodes, 0, 0) - return nodes -} - -func (q *pointerQueue) appendSemanticArea(nodes []SemanticNode, parentID SemanticID, nodeIdx int) []SemanticNode { - areaIdx := nodes[nodeIdx].areaIdx - a := q.areas[areaIdx] - childStart := len(nodes) - nodes = q.appendSemanticChildren(nodes, a.firstChild) - childEnd := len(nodes) - for i := childStart; i < childEnd; i++ { - nodes = q.appendSemanticArea(nodes, a.semantic.id, i) - } - n := &nodes[nodeIdx] - n.ParentID = parentID - n.Children = nodes[childStart:childEnd] - return nodes -} - -func (q *pointerQueue) appendSemanticChildren(nodes []SemanticNode, areaIdx int) []SemanticNode { - if areaIdx == -1 { - return nodes - } - a := q.areas[areaIdx] - if semID := a.semantic.id; semID != 0 { - cnt := a.semantic.content - nodes = append(nodes, SemanticNode{ - ID: semID, - Desc: SemanticDesc{ - Bounds: a.bounds(), - Label: cnt.label, - Description: cnt.desc, - Class: cnt.class, - Gestures: cnt.gestures, - Selected: cnt.selected, - Disabled: cnt.disabled, - }, - areaIdx: areaIdx, - }) - } else { - nodes = q.appendSemanticChildren(nodes, a.firstChild) - } - return q.appendSemanticChildren(nodes, a.sibling) -} - -func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID { - ids := q.semantic.contentIDs[content] - for i, id := range ids { - if !id.used { - ids[i].used = true - return id.id - } - } - // No prior assigned ID; allocate a new one. - q.semantic.lastID++ - id := semanticID{id: q.semantic.lastID, used: true} - if q.semantic.contentIDs == nil { - q.semantic.contentIDs = make(map[semanticContent][]semanticID) - } - q.semantic.contentIDs[content] = append(q.semantic.contentIDs[content], id) - return id.id -} - -func (q *pointerQueue) ActionAt(pos f32.Point) (action system.Action, hasAction bool) { - q.hitTest(pos, func(n *hitNode) bool { - area := q.areas[n.area] - if area.action != 0 { - action = area.action - hasAction = true - return false - } - return true - }) - return action, hasAction -} - -func (q *pointerQueue) SemanticAt(pos f32.Point) (semID SemanticID, hasSemID bool) { - q.assignSemIDs() - q.hitTest(pos, func(n *hitNode) bool { - area := q.areas[n.area] - if area.semantic.id != 0 { - semID = area.semantic.id - hasSemID = true - return false - } - return true - }) - return semID, hasSemID -} - -// hitTest searches the hit tree for nodes matching pos. Any node matching pos will -// have the onNode func invoked on it to allow the caller to extract whatever information -// is necessary for further processing. onNode may return false to terminate the walk of -// the hit tree, or true to continue. Providing this algorithm in this generic way -// allows normal event routing and system action event routing to share the same traversal -// logic even though they are interested in different aspects of hit nodes. -func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode) bool) pointer.Cursor { - // Track whether we're passing through hits. - pass := true - idx := len(q.hitTree) - 1 - cursor := pointer.CursorDefault - for idx >= 0 { - n := &q.hitTree[idx] - hit, c := q.hit(n.area, pos) - if !hit { - idx-- - continue - } - if cursor == pointer.CursorDefault { - cursor = c - } - pass = pass && n.pass - if pass { - idx-- - } else { - idx = n.next - } - if !onNode(n) { - break - } - } - return cursor -} - -func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point { - if areaIdx == -1 { - return p - } - return q.areas[areaIdx].trans.Invert().Transform(p) -} - -func (q *pointerQueue) hit(areaIdx int, p f32.Point) (bool, pointer.Cursor) { - c := pointer.CursorDefault - for areaIdx != -1 { - a := &q.areas[areaIdx] - if c == pointer.CursorDefault { - c = a.cursor - } - p := a.trans.Invert().Transform(p) - if !a.area.Hit(p) { - return false, c - } - areaIdx = a.parent - } - return true, c -} - -func (q *pointerQueue) reset() { - q.hitTree = q.hitTree[:0] - q.areas = q.areas[:0] - q.semantic.idsAssigned = false - for k, ids := range q.semantic.contentIDs { - for i := len(ids) - 1; i >= 0; i-- { - if !ids[i].used { - ids = slices.Delete(ids, i, i+1) - } else { - ids[i].used = false - } - } - if len(ids) > 0 { - q.semantic.contentIDs[k] = ids - } else { - delete(q.semantic.contentIDs, k) - } - } -} - -func (q *pointerQueue) Frame(handlers map[event.Tag]*handler, state pointerState) (pointerState, []taggedEvent) { - for _, h := range handlers { - if h.pointer.areaPlusOne != 0 { - area := &q.areas[h.pointer.areaPlusOne-1] - if h.filter.pointer.kinds&(pointer.Press|pointer.Release) != 0 { - area.semantic.content.gestures |= ClickGesture - } - if h.filter.pointer.kinds&pointer.Scroll != 0 { - area.semantic.content.gestures |= ScrollGesture - } - area.semantic.valid = area.semantic.content.gestures != 0 - } - } - var evts []taggedEvent - for i, p := range state.pointers { - changed := false - p, evts, state.cursor, changed = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, p.last) - if changed { - state.pointers = slices.Clone(state.pointers) - state.pointers[i] = p - } - } - return state, evts -} - -func dropHandler(state pointerState, tag event.Tag) pointerState { - pointers := state.pointers - state.pointers = nil - for _, p := range pointers { - handlers := p.handlers - p.handlers = nil - for _, h := range handlers { - if h != tag { - p.handlers = append(p.handlers, h) - } - } - entered := p.entered - p.entered = nil - for _, h := range entered { - if h != tag { - p.entered = append(p.entered, h) - } - } - state.pointers = append(state.pointers, p) - } - return state -} - -// pointerOf returns the pointerInfo index corresponding to the pointer in e. -func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) { - for i, p := range s.pointers { - if p.id == e.PointerID { - return s, i - } - } - n := len(s.pointers) - s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID}) - return s, len(s.pointers) - 1 -} - -// Deliver is like Push, but delivers an event to a particular area. -func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e pointer.Event) []taggedEvent { - scroll := e.Scroll - idx := len(q.hitTree) - 1 - // Locate first potential receiver. - for idx != -1 { - n := &q.hitTree[idx] - if n.area == areaIdx { - break - } - idx-- - } - var evts []taggedEvent - for idx != -1 { - n := &q.hitTree[idx] - idx = n.next - h, ok := handlers[n.tag] - if !ok || !h.filter.pointer.Matches(e) { - continue - } - e := e - if e.Kind == pointer.Scroll { - if scroll == (f32.Point{}) { - break - } - scroll, e.Scroll = h.filter.pointer.clampScroll(scroll) - } - e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) - evts = append(evts, taggedEvent{tag: n.tag, event: e}) - if e.Kind != pointer.Scroll { - break - } - } - return evts -} - -// SemanticArea returns the sematic content for area, and its parent area. -func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) { - for areaIdx != -1 { - a := &q.areas[areaIdx] - areaIdx = a.parent - if !a.semantic.valid { - continue - } - return a.semantic.content, areaIdx - } - return semanticContent{}, -1 -} - -func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState, e pointer.Event) (pointerState, []taggedEvent) { - var evts []taggedEvent - if e.Kind == pointer.Cancel { - for k := range handlers { - evts = append(evts, taggedEvent{ - event: pointer.Event{Kind: pointer.Cancel}, - tag: k, - }) - } - state.pointers = nil - return state, evts - } - state, pidx := state.pointerOf(e) - p := state.pointers[pidx] - - switch e.Kind { - case pointer.Press: - p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) - p.pressed = true - evts = q.deliverEvent(handlers, p, evts, e) - case pointer.Move: - if p.pressed { - e.Kind = pointer.Drag - } - p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) - evts = q.deliverEvent(handlers, p, evts, e) - if p.pressed { - p, evts = q.deliverDragEvent(handlers, p, evts) - } - case pointer.Release: - evts = q.deliverEvent(handlers, p, evts, e) - p.pressed = false - p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) - p, evts = q.deliverDropEvent(handlers, p, evts) - case pointer.Scroll: - p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) - evts = q.deliverEvent(handlers, p, evts, e) - default: - panic("unsupported pointer event type") - } - - p.last = e - - if !p.pressed && len(p.entered) == 0 { - // No longer need to track pointer. - state.pointers = slices.Concat(state.pointers[:pidx:pidx], state.pointers[pidx+1:]) - } else { - state.pointers = slices.Clone(state.pointers) - state.pointers[pidx] = p - } - return state, evts -} - -func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent { - if p.pressed && len(p.handlers) == 1 { - e.Priority = pointer.Grabbed - } - scroll := e.Scroll - for _, k := range p.handlers { - h, ok := handlers[k] - if !ok { - continue - } - f := h.filter.pointer - if !f.Matches(e) { - continue - } - if e.Kind == pointer.Scroll { - if scroll == (f32.Point{}) { - return evts - } - scroll, e.Scroll = f.clampScroll(scroll) - } - e := e - e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) - evts = append(evts, taggedEvent{event: e, tag: k}) - } - return evts -} - -func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler, cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) { - changed := false - var hits []event.Tag - if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press { - // Consider non-mouse pointers leaving when they're released. - } else { - var transSrc *pointerFilter - if p.dataSource != nil { - transSrc = &handlers[p.dataSource].filter.pointer - } - cursor = q.hitTest(e.Position, func(n *hitNode) bool { - h, ok := handlers[n.tag] - if !ok { - return true - } - add := true - if p.pressed { - add = false - // Filter out non-participating handlers, - // except potential transfer targets when a transfer has been initiated. - if _, found := searchTag(p.handlers, n.tag); found { - add = true - } - if transSrc != nil { - if _, ok := firstMimeMatch(transSrc, &h.filter.pointer); ok { - add = true - } - } - } - if add { - hits = addHandler(hits, n.tag) - } - return true - }) - if !p.pressed { - changed = true - p.handlers = hits - } - } - // Deliver Leave events. - for _, k := range p.entered { - if _, found := searchTag(hits, k); found { - continue - } - h, ok := handlers[k] - if !ok { - continue - } - changed = true - e := e - e.Kind = pointer.Leave - - if h.filter.pointer.Matches(e) { - e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) - evts = append(evts, taggedEvent{tag: k, event: e}) - } - } - // Deliver Enter events. - for _, k := range hits { - if _, found := searchTag(p.entered, k); found { - continue - } - h, ok := handlers[k] - if !ok { - continue - } - changed = true - e := e - e.Kind = pointer.Enter - - if h.filter.pointer.Matches(e) { - e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position) - evts = append(evts, taggedEvent{tag: k, event: e}) - } - } - p.entered = hits - return p, evts, cursor, changed -} - -func (q *pointerQueue) deliverDragEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) { - if p.dataSource != nil { - return p, evts - } - // Identify the data source. - for _, k := range p.entered { - src := &handlers[k].filter.pointer - if len(src.sourceMimes) == 0 { - continue - } - // One data source handler per pointer. - p.dataSource = k - // Notify all potential targets. - for k, tgt := range handlers { - if _, ok := firstMimeMatch(src, &tgt.filter.pointer); ok { - evts = append(evts, taggedEvent{tag: k, event: transfer.InitiateEvent{}}) - } - } - break - } - return p, evts -} - -func (q *pointerQueue) deliverDropEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) { - if p.dataSource == nil { - return p, evts - } - // Request data from the source. - src := &handlers[p.dataSource].filter.pointer - for _, k := range p.entered { - h := handlers[k] - if m, ok := firstMimeMatch(src, &h.filter.pointer); ok { - p.dataTarget = k - evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.RequestEvent{Type: m}}) - return p, evts - } - } - // No valid target found, abort. - return q.deliverTransferCancelEvent(handlers, p, evts) -} - -func (q *pointerQueue) deliverTransferCancelEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) { - evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.CancelEvent{}}) - // Cancel all potential targets. - src := &handlers[p.dataSource].filter.pointer - for k, h := range handlers { - if _, ok := firstMimeMatch(src, &h.filter.pointer); ok { - evts = append(evts, taggedEvent{tag: k, event: transfer.CancelEvent{}}) - } - } - p.dataSource = nil - p.dataTarget = nil - return p, evts -} - -// ClipFor clips r to the parents of area. -func (q *pointerQueue) ClipFor(area int, r image.Rectangle) image.Rectangle { - a := &q.areas[area] - parent := a.parent - for parent != -1 { - a := &q.areas[parent] - r = r.Intersect(a.bounds()) - parent = a.parent - } - return r -} - -func searchTag(tags []event.Tag, tag event.Tag) (int, bool) { - for i, t := range tags { - if t == tag { - return i, true - } - } - return 0, false -} - -// addHandler adds tag to the slice if not present. -func addHandler(tags []event.Tag, tag event.Tag) []event.Tag { - if slices.Contains(tags, tag) { - return tags - } - return append(tags, tag) -} - -// firstMimeMatch returns the first type match between src and tgt. -func firstMimeMatch(src, tgt *pointerFilter) (first string, matched bool) { - for _, m1 := range tgt.targetMimes { - if slices.Contains(src.sourceMimes, m1) { - return m1, true - } - } - return "", false -} - -func (op *areaOp) Hit(pos f32.Point) bool { - pos = pos.Sub(f32internal.FPt(op.rect.Min)) - size := f32internal.FPt(op.rect.Size()) - switch op.kind { - case areaRect: - return 0 <= pos.X && pos.X < size.X && - 0 <= pos.Y && pos.Y < size.Y - case areaEllipse: - rx := size.X / 2 - ry := size.Y / 2 - xh := pos.X - rx - yk := pos.Y - ry - // The ellipse function works in all cases because - // 0/0 is not <= 1. - return (xh*xh)/(rx*rx)+(yk*yk)/(ry*ry) <= 1 - default: - panic("invalid area kind") - } -} - -func (a *areaNode) bounds() image.Rectangle { - return f32internal.Rectangle{ - Min: a.trans.Transform(f32internal.FPt(a.area.rect.Min)), - Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)), - }.Round() -} diff --git a/gio/io/input/pointer_test.go b/gio/io/input/pointer_test.go deleted file mode 100644 index d78cbf6..0000000 --- a/gio/io/input/pointer_test.go +++ /dev/null @@ -1,1347 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package input - -import ( - "fmt" - "image" - "reflect" - "strings" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -func TestFilterReset(t *testing.T) { - r := new(Router) - if _, ok := r.Event(pointer.Filter{}); ok { - t.Fatal("empty filter matched reset event") - } - if _, ok := r.Event(pointer.Filter{Kinds: pointer.Cancel}); ok { - t.Fatal("second call to Event matched reset event") - } -} - -func TestPointerNilTarget(t *testing.T) { - r := new(Router) - r.Event(pointer.Filter{Kinds: pointer.Press}) - r.Frame(new(op.Ops)) - r.Queue(pointer.Event{Kind: pointer.Press}) - // Nil Targets should not receive events. - if _, ok := r.Event(pointer.Filter{Kinds: pointer.Press}); ok { - t.Errorf("nil target received event") - } -} - -func TestPointerWakeup(t *testing.T) { - handler := new(int) - var ops op.Ops - var r Router - addPointerHandler(&r, &ops, handler, image.Rect(0, 0, 100, 100)) - - // Test that merely adding a handler doesn't trigger redraw. - r.Frame(&ops) - if _, wake := r.WakeupTime(); wake { - t.Errorf("adding pointer.InputOp triggered a redraw") - } -} - -func TestPointerDrag(t *testing.T) { - handler := new(int) - var ops op.Ops - var r Router - f := addPointerHandler(&r, &ops, handler, image.Rect(0, 0, 100, 100)) - - r.Frame(&ops) - r.Queue( - // Press. - pointer.Event{ - Kind: pointer.Press, - Position: f32.Pt(50, 50), - }, - // Move outside the area. - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(150, 150), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag) -} - -func TestPointerDragNegative(t *testing.T) { - handler := new(int) - var ops op.Ops - var r Router - f := addPointerHandler(&r, &ops, handler, image.Rect(-100, -100, 0, 0)) - - r.Frame(&ops) - r.Queue( - // Press. - pointer.Event{ - Kind: pointer.Press, - Position: f32.Pt(-50, -50), - }, - // Move outside the area. - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(-150, -150), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag) -} - -func TestIgnoredGrab(t *testing.T) { - handler1 := new(int) - handler2 := new(int) - var ops op.Ops - - filter := func(t event.Tag) event.Filter { - return pointer.Filter{Target: t, Kinds: pointer.Press | pointer.Release | pointer.Cancel} - } - - event.Op(&ops, handler1) - event.Op(&ops, handler2) - var r Router - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Cancel) - r.Frame(&ops) - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Position: f32.Pt(50, 50), - }, - pointer.Event{ - Kind: pointer.Release, - Position: f32.Pt(50, 50), - }, - ) - assertEventPointerTypeSequence(t, events(&r, 1, filter(handler1)), pointer.Press) - assertEventPointerTypeSequence(t, events(&r, 1, filter(handler2)), pointer.Press) - r.Source().Execute(pointer.GrabCmd{Tag: handler1}) - r.Source().Execute(pointer.GrabCmd{Tag: handler2}) - assertEventPointerTypeSequence(t, events(&r, 1, filter(handler1)), pointer.Release) - assertEventPointerTypeSequence(t, events(&r, 1, filter(handler2)), pointer.Cancel) -} - -func TestPointerGrab(t *testing.T) { - handler1 := new(int) - handler2 := new(int) - handler3 := new(int) - var ops op.Ops - - filter := func(t event.Tag) event.Filter { - return pointer.Filter{Target: t, Kinds: pointer.Press | pointer.Release | pointer.Cancel} - } - - event.Op(&ops, handler1) - event.Op(&ops, handler2) - event.Op(&ops, handler3) - - var r Router - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler3)), pointer.Cancel) - r.Frame(&ops) - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Position: f32.Pt(50, 50), - }, - ) - assertEventPointerTypeSequence(t, events(&r, 1, filter(handler1)), pointer.Press) - assertEventPointerTypeSequence(t, events(&r, 1, filter(handler2)), pointer.Press) - assertEventPointerTypeSequence(t, events(&r, 1, filter(handler3)), pointer.Press) - r.Source().Execute(pointer.GrabCmd{Tag: handler1}) - r.Queue( - pointer.Event{ - Kind: pointer.Release, - Position: f32.Pt(50, 50), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Release) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler3)), pointer.Cancel) -} - -func TestPointerGrabSameHandlerTwice(t *testing.T) { - handler1 := new(int) - handler2 := new(int) - var ops op.Ops - - filter := func(t event.Tag) event.Filter { - return pointer.Filter{Target: t, Kinds: pointer.Press | pointer.Release | pointer.Cancel} - } - - event.Op(&ops, handler1) - event.Op(&ops, handler1) - event.Op(&ops, handler2) - - var r Router - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Cancel) - r.Frame(&ops) - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Position: f32.Pt(50, 50), - }, - ) - assertEventPointerTypeSequence(t, events(&r, 1, filter(handler1)), pointer.Press) - assertEventPointerTypeSequence(t, events(&r, 1, filter(handler2)), pointer.Press) - r.Source().Execute(pointer.GrabCmd{Tag: handler1}) - r.Queue( - pointer.Event{ - Kind: pointer.Release, - Position: f32.Pt(50, 50), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Release) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Cancel) -} - -func TestPointerMove(t *testing.T) { - handler1 := new(int) - handler2 := new(int) - var ops op.Ops - - filter := func(t event.Tag) event.Filter { - return pointer.Filter{ - Target: t, - Kinds: pointer.Move | pointer.Enter | pointer.Leave | pointer.Cancel, - } - } - - // Handler 1 area: (0, 0) - (100, 100) - r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - event.Op(&ops, handler1) - // Handler 2 area: (50, 50) - (100, 100) (areas intersect). - r2 := clip.Rect(image.Rect(50, 50, 200, 200)).Push(&ops) - event.Op(&ops, handler2) - r2.Pop() - r1.Pop() - - var r Router - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Cancel) - r.Frame(&ops) - r.Queue( - // Hit both handlers. - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(50, 50), - }, - // Hit handler 1. - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(49, 50), - }, - // Hit no handlers. - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(100, 50), - }, - pointer.Event{ - Kind: pointer.Cancel, - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Enter, pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Enter, pointer.Move, pointer.Leave, pointer.Cancel) -} - -func TestPointerTypes(t *testing.T) { - handler := new(int) - var ops op.Ops - r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - f := pointer.Filter{ - Target: handler, - Kinds: pointer.Press | pointer.Release | pointer.Cancel, - } - event.Op(&ops, handler) - r1.Pop() - - var r Router - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Cancel) - r.Frame(&ops) - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Position: f32.Pt(50, 50), - }, - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(150, 150), - }, - pointer.Event{ - Kind: pointer.Release, - Position: f32.Pt(150, 150), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Press, pointer.Release) -} - -func TestPointerSystemAction(t *testing.T) { - t.Run("simple", func(t *testing.T) { - var ops op.Ops - r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - system.ActionInputOp(system.ActionMove).Add(&ops) - r1.Pop() - - var r Router - r.Frame(&ops) - assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove) - }) - t.Run("covered by another clip", func(t *testing.T) { - var ops op.Ops - r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - system.ActionInputOp(system.ActionMove).Add(&ops) - clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop() - r1.Pop() - - var r Router - r.Frame(&ops) - assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove) - }) - t.Run("uses topmost action op", func(t *testing.T) { - var ops op.Ops - r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - system.ActionInputOp(system.ActionMove).Add(&ops) - r2 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - system.ActionInputOp(system.ActionClose).Add(&ops) - r2.Pop() - r1.Pop() - - var r Router - r.Frame(&ops) - assertActionAt(t, r, f32.Pt(50, 50), system.ActionClose) - }) -} - -func TestPointerPriority(t *testing.T) { - handler1 := new(int) - handler2 := new(int) - handler3 := new(int) - var ops op.Ops - var r Router - - r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - f1 := func(t event.Tag) event.Filter { - return pointer.Filter{ - Target: t, - Kinds: pointer.Scroll, - ScrollX: pointer.ScrollRange{Max: 100}, - } - } - events(&r, -1, f1(handler1)) - event.Op(&ops, handler1) - - r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops) - f2 := func(t event.Tag) event.Filter { - return pointer.Filter{ - Target: t, - Kinds: pointer.Scroll, - ScrollX: pointer.ScrollRange{Max: 20}, - } - } - events(&r, -1, f2(handler2)) - event.Op(&ops, handler2) - r2.Pop() - r1.Pop() - - r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops) - f3 := func(t event.Tag) event.Filter { - return pointer.Filter{ - Target: t, - Kinds: pointer.Scroll, - ScrollX: pointer.ScrollRange{Min: -20}, - ScrollY: pointer.ScrollRange{Min: -40}, - } - } - events(&r, -1, f3(handler3)) - event.Op(&ops, handler3) - r3.Pop() - - r.Frame(&ops) - r.Queue( - // Hit handler 1 and 2. - pointer.Event{ - Kind: pointer.Scroll, - Position: f32.Pt(50, 25), - Scroll: f32.Pt(50, 0), - }, - // Hit handler 1. - pointer.Event{ - Kind: pointer.Scroll, - Position: f32.Pt(50, 75), - Scroll: f32.Pt(50, 50), - }, - // Hit handler 3. - pointer.Event{ - Kind: pointer.Scroll, - Position: f32.Pt(50, 150), - Scroll: f32.Pt(-30, -30), - }, - // Hit no handlers. - pointer.Event{ - Kind: pointer.Scroll, - Position: f32.Pt(50, 225), - }, - ) - - hev1 := events(&r, -1, f1(handler1)) - hev2 := events(&r, -1, f2(handler2)) - hev3 := events(&r, -1, f3(handler3)) - assertEventPointerTypeSequence(t, hev1, pointer.Scroll, pointer.Scroll) - assertEventPointerTypeSequence(t, hev2, pointer.Scroll) - assertEventPointerTypeSequence(t, hev3, pointer.Scroll) - assertEventPriorities(t, hev1, pointer.Shared, pointer.Shared) - assertEventPriorities(t, hev2, pointer.Shared) - assertEventPriorities(t, hev3, pointer.Shared) - assertScrollEvent(t, hev1[0], f32.Pt(30, 0)) - assertScrollEvent(t, hev2[0], f32.Pt(20, 0)) - assertScrollEvent(t, hev1[1], f32.Pt(50, 0)) - assertScrollEvent(t, hev3[0], f32.Pt(-20, -30)) -} - -func TestPointerEnterLeave(t *testing.T) { - handler1 := new(int) - handler2 := new(int) - var ops op.Ops - var r Router - - // Handler 1 area: (0, 0) - (100, 100) - f1 := addPointerHandler(&r, &ops, handler1, image.Rect(0, 0, 100, 100)) - - // Handler 2 area: (50, 50) - (200, 200) (areas overlap). - f2 := addPointerHandler(&r, &ops, handler2, image.Rect(50, 50, 200, 200)) - - r.Frame(&ops) - // Hit both handlers. - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(50, 50), - }, - ) - // First event for a handler is always a Cancel. - // Only handler2 should receive the enter/move events because it is on top - // and handler1 is not an ancestor in the hit tree. - assertEventPointerTypeSequence(t, events(&r, -1, f1)) - assertEventPointerTypeSequence(t, events(&r, -1, f2), pointer.Enter, pointer.Move) - - // Leave the second area by moving into the first. - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(45, 45), - }, - ) - // The cursor leaves handler2 and enters handler1. - assertEventPointerTypeSequence(t, events(&r, -1, f1), pointer.Enter, pointer.Move) - assertEventPointerTypeSequence(t, events(&r, -1, f2), pointer.Leave) - - // Move, but stay within the same hit area. - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(40, 40), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f1), pointer.Move) - assertEventPointerTypeSequence(t, events(&r, -1, f2)) - - // Move outside of both inputs. - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(300, 300), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f1), pointer.Leave) - assertEventPointerTypeSequence(t, events(&r, -1, f2)) - - // Check that a Press event generates Enter Events. - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Position: f32.Pt(125, 125), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f1)) - assertEventPointerTypeSequence(t, events(&r, -1, f2), pointer.Enter, pointer.Press) - - // Check that a drag only affects the participating handlers. - r.Queue( - // Leave - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(25, 25), - }, - // Enter - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(50, 50), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f1)) - assertEventPointerTypeSequence(t, events(&r, -1, f2), pointer.Leave, pointer.Drag, pointer.Enter, pointer.Drag) - - // Check that a Release event generates Enter/Leave Events. - r.Queue( - pointer.Event{ - Kind: pointer.Release, - Position: f32.Pt(25, - 25), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f1), pointer.Enter) - // The second handler gets the release event because the press started inside it. - assertEventPointerTypeSequence(t, events(&r, -1, f2), pointer.Release, pointer.Leave) -} - -func TestMultipleAreas(t *testing.T) { - handler := new(int) - - var ops op.Ops - var r Router - - f := addPointerHandler(&r, &ops, handler, image.Rect(0, 0, 100, 100)) - r1 := clip.Rect(image.Rect(50, 50, 200, 200)).Push(&ops) - // Test that declaring a handler twice doesn't affect event handling. - event.Op(&ops, handler) - r1.Pop() - - assertEventPointerTypeSequence(t, events(&r, -1, f)) - r.Frame(&ops) - // Hit first area, then second area, then both. - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(25, 25), - }, - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(150, 150), - }, - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(50, 50), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Enter, pointer.Move, pointer.Move, pointer.Move) -} - -func TestPointerEnterLeaveNested(t *testing.T) { - handler1 := new(int) - handler2 := new(int) - var ops op.Ops - - filter := func(t event.Tag) event.Filter { - return pointer.Filter{ - Target: t, - Kinds: pointer.Press | pointer.Move | pointer.Release | pointer.Enter | pointer.Leave | pointer.Cancel, - } - } - - // Handler 1 area: (0, 0) - (100, 100) - r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - event.Op(&ops, handler1) - - // Handler 2 area: (25, 25) - (75, 75) (nested within first). - r2 := clip.Rect(image.Rect(25, 25, 75, 75)).Push(&ops) - event.Op(&ops, handler2) - r2.Pop() - r1.Pop() - - var r Router - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Cancel) - r.Frame(&ops) - // Hit both handlers. - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(50, 50), - }, - ) - // First event for a handler is always a Cancel. - // Both handlers should receive the Enter and Move events because handler2 is a child of handler1. - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Enter, pointer.Move) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Enter, pointer.Move) - - // Leave the second area by moving into the first. - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(20, 20), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Move) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Leave) - - // Move, but stay within the same hit area. - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(10, 10), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Move) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2))) - - // Move outside of both inputs. - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(200, 200), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Leave) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2))) - - // Check that a Press event generates Enter Events. - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Position: f32.Pt(50, 50), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Enter, pointer.Press) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Enter, pointer.Press) - - // Check that a Release event generates Enter/Leave Events. - r.Queue( - pointer.Event{ - Kind: pointer.Release, - Position: f32.Pt(20, 20), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler1)), pointer.Release) - assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Release, pointer.Leave) -} - -func TestPointerActiveInputDisappears(t *testing.T) { - handler1 := new(int) - var ops op.Ops - var r Router - - // Draw handler. - ops.Reset() - f := addPointerHandler(&r, &ops, handler1, image.Rect(0, 0, 100, 100)) - r.Frame(&ops) - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(25, 25), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Enter, pointer.Move) - r.Frame(&ops) - - // Re-render with handler missing. - ops.Reset() - r.Frame(&ops) - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(25, 25), - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Cancel) -} - -func TestMultitouch(t *testing.T) { - var ops op.Ops - var r Router - - // Add two separate handlers. - h1, h2 := new(int), new(int) - f1 := addPointerHandler(&r, &ops, h1, image.Rect(0, 0, 100, 100)) - f2 := addPointerHandler(&r, &ops, h2, image.Rect(0, 100, 100, 200)) - - h1pt, h2pt := f32.Pt(0, 0), f32.Pt(0, 100) - var p1, p2 pointer.ID = 0, 1 - - r.Frame(&ops) - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Position: h1pt, - PointerID: p1, - }, - ) - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Position: h2pt, - PointerID: p2, - }, - ) - r.Queue( - pointer.Event{ - Kind: pointer.Release, - Position: h2pt, - PointerID: p2, - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f1), pointer.Enter, pointer.Press) - assertEventPointerTypeSequence(t, events(&r, -1, f2), pointer.Enter, pointer.Press, pointer.Release) -} - -func TestCursor(t *testing.T) { - _at := func(x, y float32) []event.Event { - return []event.Event{pointer.Event{ - Kind: pointer.Move, - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - Position: f32.Pt(x, y), - }} - } - ops := new(op.Ops) - var r Router - for _, tc := range []struct { - label string - events []event.Event - cursors []pointer.Cursor - want pointer.Cursor - }{ - { - label: "no movement", - cursors: []pointer.Cursor{pointer.CursorPointer}, - want: pointer.CursorDefault, - }, - { - label: "move inside", - cursors: []pointer.Cursor{pointer.CursorPointer}, - events: _at(50, 50), - want: pointer.CursorPointer, - }, - { - label: "move outside", - cursors: []pointer.Cursor{pointer.CursorPointer}, - events: _at(200, 200), - want: pointer.CursorDefault, - }, - { - label: "move back inside", - cursors: []pointer.Cursor{pointer.CursorPointer}, - events: _at(50, 50), - want: pointer.CursorPointer, - }, - { - label: "send key events while inside", - cursors: []pointer.Cursor{pointer.CursorPointer}, - events: []event.Event{ - key.Event{Name: "A", State: key.Press}, - key.Event{Name: "A", State: key.Release}, - }, - want: pointer.CursorPointer, - }, - { - label: "send key events while outside", - cursors: []pointer.Cursor{pointer.CursorPointer}, - events: append( - _at(200, 200), - key.Event{Name: "A", State: key.Press}, - key.Event{Name: "A", State: key.Release}, - ), - want: pointer.CursorDefault, - }, - { - label: "add new input on top while inside", - cursors: []pointer.Cursor{pointer.CursorPointer, pointer.CursorCrosshair}, - events: append( - _at(50, 50), - key.Event{ - Name: "A", - State: key.Press, - }, - ), - want: pointer.CursorCrosshair, - }, - { - label: "remove input on top while inside", - cursors: []pointer.Cursor{pointer.CursorPointer}, - events: append( - _at(50, 50), - key.Event{ - Name: "A", - State: key.Press, - }, - ), - want: pointer.CursorPointer, - }, - } { - t.Run(tc.label, func(t *testing.T) { - ops.Reset() - defer clip.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Push(ops).Pop() - for _, c := range tc.cursors { - c.Add(ops) - } - r.Frame(ops) - r.Queue(tc.events...) - // The cursor should now have been changed if the mouse moved over the declared area. - if got, want := r.Cursor(), tc.want; got != want { - t.Errorf("got %q; want %q", got, want) - } - }) - } -} - -func TestPassOp(t *testing.T) { - var ops op.Ops - - h1, h2, h3, h4 := new(int), new(int), new(int), new(int) - area := clip.Rect(image.Rect(0, 0, 100, 100)) - root := area.Push(&ops) - event.Op(&ops, &h1) - event.Op(&ops, h1) - child1 := area.Push(&ops) - event.Op(&ops, h2) - child1.Pop() - child2 := area.Push(&ops) - pass := pointer.PassOp{}.Push(&ops) - event.Op(&ops, h3) - event.Op(&ops, h4) - pass.Pop() - child2.Pop() - root.Pop() - - var r Router - filter := func(t event.Tag) event.Filter { - return pointer.Filter{Target: t, Kinds: pointer.Press | pointer.Cancel} - } - assertEventPointerTypeSequence(t, events(&r, -1, filter(h1)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(h2)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(h3)), pointer.Cancel) - assertEventPointerTypeSequence(t, events(&r, -1, filter(h4)), pointer.Cancel) - r.Frame(&ops) - r.Queue( - pointer.Event{ - Kind: pointer.Press, - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, filter(h1)), pointer.Press) - assertEventPointerTypeSequence(t, events(&r, -1, filter(h2)), pointer.Press) - assertEventPointerTypeSequence(t, events(&r, -1, filter(h3)), pointer.Press) - assertEventPointerTypeSequence(t, events(&r, -1, filter(h4)), pointer.Press) -} - -func TestAreaPassthrough(t *testing.T) { - var ops op.Ops - - h := new(int) - event.Op(&ops, h) - clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop() - var r Router - f := pointer.Filter{ - Target: h, - Kinds: pointer.Press | pointer.Cancel, - } - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Cancel) - r.Frame(&ops) - r.Queue( - pointer.Event{ - Kind: pointer.Press, - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Press) -} - -func TestEllipse(t *testing.T) { - var ops op.Ops - - h := new(int) - cl := clip.Ellipse(image.Rect(0, 0, 100, 100)).Push(&ops) - event.Op(&ops, h) - cl.Pop() - var r Router - f := pointer.Filter{ - Target: h, - Kinds: pointer.Press | pointer.Cancel, - } - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Cancel) - r.Frame(&ops) - r.Queue( - // Outside ellipse. - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Press, - }, - pointer.Event{ - Kind: pointer.Release, - }, - // Inside ellipse. - pointer.Event{ - Position: f32.Pt(50, 50), - Kind: pointer.Press, - }, - ) - assertEventPointerTypeSequence(t, events(&r, -1, f), pointer.Press) -} - -func TestTransfer(t *testing.T) { - srcArea := image.Rect(0, 0, 20, 20) - tgtArea := srcArea.Add(image.Pt(40, 0)) - setup := func(r *Router, ops *op.Ops, srcType, tgtType string) (src, tgt event.Tag) { - src, tgt = new(int), new(int) - events(r, -1, transfer.SourceFilter{Target: src, Type: srcType}) - events(r, -1, transfer.TargetFilter{Target: tgt, Type: tgtType}) - - srcStack := clip.Rect(srcArea).Push(ops) - event.Op(ops, src) - srcStack.Pop() - - tgt1Stack := clip.Rect(tgtArea).Push(ops) - event.Op(ops, tgt) - tgt1Stack.Pop() - - return src, tgt - } - - t.Run("drop on no target", func(t *testing.T) { - ops := new(op.Ops) - var r Router - src, tgt := setup(&r, ops, "file", "file") - r.Frame(ops) - // Initiate a drag. - r.Queue( - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Press, - }, - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Move, - }, - ) - assertEventSequence(t, events(&r, -1, transfer.SourceFilter{Target: src, Type: "file"})) - assertEventSequence(t, events(&r, -1, transfer.TargetFilter{Target: tgt, Type: "file"}), transfer.InitiateEvent{}) - - // Drop. - r.Queue( - pointer.Event{ - Position: f32.Pt(30, 10), - Kind: pointer.Move, - }, - pointer.Event{ - Position: f32.Pt(30, 10), - Kind: pointer.Release, - }, - ) - assertEventSequence(t, events(&r, -1, transfer.SourceFilter{Target: src, Type: "file"}), transfer.CancelEvent{}) - assertEventSequence(t, events(&r, -1, transfer.TargetFilter{Target: tgt, Type: "file"}), transfer.CancelEvent{}) - }) - - t.Run("drag with valid and invalid targets", func(t *testing.T) { - ops := new(op.Ops) - var r Router - src, tgt1 := setup(&r, ops, "file", "file") - tgt2 := new(int) - events(&r, -1, transfer.TargetFilter{Target: tgt2, Type: "nofile"}) - stack := clip.Rect(tgtArea).Push(ops) - event.Op(ops, tgt2) - stack.Pop() - r.Frame(ops) - // Initiate a drag. - r.Queue( - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Press, - }, - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Move, - }, - ) - assertEventSequence(t, events(&r, -1, transfer.SourceFilter{Target: src, Type: "file"})) - assertEventSequence(t, events(&r, -1, transfer.TargetFilter{Target: tgt1, Type: "file"}), transfer.InitiateEvent{}) - assertEventSequence(t, events(&r, -1, transfer.TargetFilter{Target: tgt2, Type: "nofile"})) - }) - - t.Run("drop on invalid target", func(t *testing.T) { - ops := new(op.Ops) - var r Router - src, tgt := setup(&r, ops, "file", "nofile") - r.Frame(ops) - // Drag. - r.Queue( - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Press, - }, - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Move, - }, - ) - assertEventSequence(t, events(&r, -1, transfer.SourceFilter{Target: src, Type: "file"})) - assertEventSequence(t, events(&r, -1, transfer.TargetFilter{Target: tgt, Type: "nofile"})) - - // Drop. - r.Queue( - pointer.Event{ - Position: f32.Pt(40, 10), - Kind: pointer.Release, - }, - ) - assertEventSequence(t, events(&r, -1, transfer.SourceFilter{Target: src, Type: "file"}), transfer.CancelEvent{}) - assertEventSequence(t, events(&r, -1, transfer.TargetFilter{Target: tgt, Type: "nofile"})) - }) - - t.Run("drop on valid target", func(t *testing.T) { - ops := new(op.Ops) - var r Router - src, tgt := setup(&r, ops, "file", "file") - // Make the target also a source. This should have no effect. - events(&r, -1, transfer.SourceFilter{Target: tgt, Type: "file"}) - r.Frame(ops) - // Drag. - r.Queue( - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Press, - }, - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Move, - }, - ) - assertEventSequence(t, events(&r, 1, transfer.TargetFilter{Target: tgt, Type: "file"}), transfer.InitiateEvent{}) - - // Drop. - r.Queue( - pointer.Event{ - Position: f32.Pt(40, 10), - Kind: pointer.Release, - }, - ) - assertEventSequence(t, events(&r, 1, transfer.SourceFilter{Target: src, Type: "file"}), transfer.RequestEvent{Type: "file"}) - - // Offer valid type and data. - ofr := &offer{data: "hello"} - r.Source().Execute(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr}) - assertEventSequence(t, events(&r, -1, transfer.SourceFilter{Target: src, Type: "file"}), transfer.CancelEvent{}) - evs := events(&r, -1, transfer.TargetFilter{Target: tgt, Type: "file"}) - if len(evs) != 2 { - t.Fatalf("unexpected number of events: %d, want 2", len(evs)) - } - assertEventSequence(t, evs[1:], transfer.CancelEvent{}) - dataEvent, ok := evs[0].(transfer.DataEvent) - if !ok { - t.Fatalf("unexpected event type: %T, want %T", dataEvent, transfer.DataEvent{}) - } - if got, want := dataEvent.Type, "file"; got != want { - t.Fatalf("got %s; want %s", got, want) - } - if got, want := dataEvent.Open(), ofr; got != want { - t.Fatalf("got %v; want %v", got, want) - } - - // Drag and drop complete. - if ofr.closed { - t.Error("offer closed prematurely") - } - }) - - t.Run("drop on valid target, DataEvent not used", func(t *testing.T) { - ops := new(op.Ops) - var r Router - src, tgt := setup(&r, ops, "file", "file") - // Make the target also a source. This should have no effect. - events(&r, -1, transfer.SourceFilter{Target: tgt, Type: "file"}) - r.Frame(ops) - // Drag. - r.Queue( - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Press, - }, - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Move, - }, - pointer.Event{ - Position: f32.Pt(40, 10), - Kind: pointer.Release, - }, - ) - ofr := &offer{data: "hello"} - events(&r, -1, transfer.SourceFilter{Target: src, Type: "file"}) - events(&r, -1, transfer.TargetFilter{Target: tgt, Type: "file"}) - r.Frame(ops) - r.Source().Execute(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr}) - assertEventSequence(t, events(&r, -1, transfer.SourceFilter{Target: src, Type: "file"}), transfer.CancelEvent{}) - // Ignore DataEvent and verify that the next frame closes it as unused. - assertEventSequence(t, events(&r, -1, transfer.TargetFilter{Target: tgt, Type: "file"})[1:], transfer.CancelEvent{}) - r.Frame(ops) - if !ofr.closed { - t.Error("offer was not closed") - } - }) -} - -func TestDeferredInputOp(t *testing.T) { - var ops op.Ops - - var r Router - m := op.Record(&ops) - event.Op(&ops, new(int)) - call := m.Stop() - - op.Defer(&ops, call) - r.Frame(&ops) -} - -func TestPassCursor(t *testing.T) { - var ops op.Ops - var r Router - - rect := clip.Rect(image.Rect(0, 0, 100, 100)) - background := rect.Push(&ops) - event.Op(&ops, 1) - pointer.CursorDefault.Add(&ops) - background.Pop() - - overlayPass := pointer.PassOp{}.Push(&ops) - overlay := rect.Push(&ops) - event.Op(&ops, 2) - want := pointer.CursorPointer - want.Add(&ops) - overlay.Pop() - overlayPass.Pop() - r.Frame(&ops) - r.Queue(pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Move, - }) - r.Frame(&ops) - if got := r.Cursor(); want != got { - t.Errorf("got cursor %v, want %v", got, want) - } -} - -func TestPartialEvent(t *testing.T) { - var ops op.Ops - var r Router - - rect := clip.Rect(image.Rect(0, 0, 100, 100)) - background := rect.Push(&ops) - event.Op(&ops, 1) - background.Pop() - - overlayPass := pointer.PassOp{}.Push(&ops) - overlay := rect.Push(&ops) - event.Op(&ops, 2) - overlay.Pop() - overlayPass.Pop() - assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 1, Kinds: pointer.Press})) - assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 2, Kinds: pointer.Press})) - r.Frame(&ops) - r.Queue(pointer.Event{ - Kind: pointer.Press, - }) - assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 1, Kinds: pointer.Press}, key.FocusFilter{Target: 1}), - key.FocusEvent{}, pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Priority: pointer.Shared}) - r.Source().Execute(key.FocusCmd{Tag: 1}) - assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 2, Kinds: pointer.Press}), - pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Priority: pointer.Shared}) -} - -// offer satisfies io.ReadCloser for use in data transfers. -type offer struct { - data string - closed bool -} - -func (offer) Read([]byte) (int, error) { return 0, nil } -func (o *offer) Close() error { - o.closed = true - return nil -} - -// addPointerHandler adds a pointer.InputOp for the tag in a -// rectangular area. -func addPointerHandler(r *Router, ops *op.Ops, tag event.Tag, area image.Rectangle) pointer.Filter { - f := pointer.Filter{ - Target: tag, - Kinds: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave | pointer.Cancel, - } - events(r, -1, f) - defer clip.Rect(area).Push(ops).Pop() - event.Op(ops, tag) - return f -} - -// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes -// that all input events are of underlying type pointer.Event, and thus will -// panic if some are not. -func pointerTypes(events []event.Event) []pointer.Kind { - var types []pointer.Kind - for _, e := range events { - if e, ok := e.(pointer.Event); ok { - types = append(types, e.Kind) - } - } - return types -} - -// assertEventPointerTypeSequence checks that the provided events match the expected pointer event types -// in the provided order. -func assertEventPointerTypeSequence(t *testing.T, events []event.Event, expected ...pointer.Kind) { - t.Helper() - got := pointerTypes(events) - if !reflect.DeepEqual(got, expected) { - t.Errorf("expected %v events, got %v", expected, got) - } -} - -// assertEventSequence checks that the provided events match the expected ones -// in the provided order. -func assertEventSequence(t *testing.T, got []event.Event, expected ...event.Event) { - t.Helper() - if len(expected) == 0 { - if len(got) > 0 { - t.Errorf("unexpected events: %v", eventsToString(got)) - } - return - } - if !reflect.DeepEqual(got, expected) { - t.Errorf("expected %s events, got %s", eventsToString(expected), eventsToString(got)) - } -} - -// assertEventTypeSequence checks that the provided event types match expected. -func assertEventTypeSequence(t *testing.T, got []event.Event, expected ...event.Event) { - t.Helper() - match := len(expected) == len(got) - if match { - for i, ge := range got { - exp := expected[i] - match = match && reflect.TypeOf(ge) == reflect.TypeOf(exp) - } - } - if !match { - t.Errorf("expected event types %s, got %s", eventTypesToString(expected), eventTypesToString(got)) - } -} - -func eventTypesToString(evs []event.Event) string { - var s []string - for _, e := range evs { - s = append(s, fmt.Sprintf("%T", e)) - } - return "[" + strings.Join(s, ",") + "]" -} - -func eventsToString(evs []event.Event) string { - var s []string - for _, ev := range evs { - switch e := ev.(type) { - case pointer.Event: - s = append(s, fmt.Sprintf("%T{%s}", e, e.Kind.String())) - default: - s = append(s, fmt.Sprintf("{%T}", e)) - } - } - return "[" + strings.Join(s, ",") + "]" -} - -// assertEventPriorities checks that the pointer.Event priorities of events match prios. -func assertEventPriorities(t *testing.T, events []event.Event, prios ...pointer.Priority) { - t.Helper() - var got []pointer.Priority - for _, e := range events { - if e, ok := e.(pointer.Event); ok { - got = append(got, e.Priority) - } - } - if !reflect.DeepEqual(got, prios) { - t.Errorf("expected priorities %v, got %v", prios, got) - } -} - -// assertScrollEvent checks that the event scrolling amount matches the supplied value. -func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) { - t.Helper() - if got, want := ev.(pointer.Event).Scroll, scroll; got != want { - t.Errorf("got %v; want %v", got, want) - } -} - -// assertActionAt checks that the router has a system action of the expected type at point. -func assertActionAt(t *testing.T, q Router, point f32.Point, expected system.Action) { - t.Helper() - action, ok := q.ActionAt(point) - if !ok { - t.Errorf("expected action %v at %v, got no action", expected, point) - } else if action != expected { - t.Errorf("expected action %v at %v, got %v", expected, point, action) - } -} - -func BenchmarkRouterAdd(b *testing.B) { - // Set this to the number of overlapping handlers that you want to - // evaluate performance for. Typical values for the example applications - // are 1-3, though checking highers values helps evaluate performance for - // more complex applications. - const startingHandlerCount = 3 - const maxHandlerCount = 100 - for i := startingHandlerCount; i < maxHandlerCount; i *= 3 { - handlerCount := i - b.Run(fmt.Sprintf("%d-handlers", i), func(b *testing.B) { - handlers := make([]event.Tag, handlerCount) - for i := range handlerCount { - h := new(int) - *h = i - handlers[i] = h - } - var ops op.Ops - - var r Router - for i := range handlers { - clip.Rect(image.Rectangle{ - Max: image.Point{ - X: 100, - Y: 100, - }, - }). - Push(&ops) - events(&r, -1, pointer.Filter{Target: handlers[i], Kinds: pointer.Move}) - event.Op(&ops, handlers[i]) - } - r.Frame(&ops) - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - r.Queue( - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(50, 50), - }, - ) - } - }) - } -} - -func events(r *Router, n int, filters ...event.Filter) []event.Event { - var events []event.Event - for { - if n != -1 && len(events) == n { - break - } - e, ok := r.Event(filters...) - if !ok { - break - } - events = append(events, e) - } - return events -} diff --git a/gio/io/input/router.go b/gio/io/input/router.go deleted file mode 100644 index 864f759..0000000 --- a/gio/io/input/router.go +++ /dev/null @@ -1,893 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package input - -import ( - "image" - "io" - "slices" - "strings" - "time" - - "github.com/p9c/p9/pkg/gel/gio/f32" - f32internal "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/io/clipboard" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Router tracks the [io/event.Tag] identifiers of user interface widgets -// and routes events to them. [Source] is its interface exposed to widgets. -type Router struct { - savedTrans []f32.Affine2D - transStack []f32.Affine2D - handlers map[event.Tag]*handler - pointer struct { - queue pointerQueue - collector pointerCollector - } - key struct { - queue keyQueue - // The following fields have the same purpose as the fields in - // type handler, but for key.Events. - filter keyFilter - nextFilter keyFilter - scratchFilter keyFilter - } - cqueue clipboardQueue - // states is the list of pending state changes resulting from - // incoming events. The first element, if present, contains the state - // and events for the current frame. - changes []stateChange - reader ops.Reader - // InvalidateCmd summary. - wakeup bool - wakeupTime time.Time - // Changes queued for next call to Frame. - commands []Command - // transfers is the pending transfer.DataEvent.Open functions. - transfers []io.ReadCloser - // deferring is set if command execution and event delivery is deferred - // to the next frame. - deferring bool - // scratchFilters is for garbage-free construction of ephemeral filters. - scratchFilters []taggedFilter -} - -// Source implements the interface between a Router and user interface widgets. -// The zero-value Source is disabled. -type Source struct { - r *Router - disabled bool -} - -// Command represents a request such as moving the focus, or initiating a clipboard read. -// Commands are queued by calling [Source.Queue]. -type Command interface { - ImplementsCommand() -} - -// SemanticNode represents a node in the tree describing the components -// contained in a frame. -type SemanticNode struct { - ID SemanticID - ParentID SemanticID - Children []SemanticNode - Desc SemanticDesc - - areaIdx int -} - -// SemanticDesc provides a semantic description of a UI component. -type SemanticDesc struct { - Class semantic.ClassOp - Description string - Label string - Selected bool - Disabled bool - Gestures SemanticGestures - Bounds image.Rectangle -} - -// SemanticGestures is a bit-set of supported gestures. -type SemanticGestures int - -const ( - ClickGesture SemanticGestures = 1 << iota - ScrollGesture -) - -// SemanticID uniquely identifies a SemanticDescription. -// -// By convention, the zero value denotes the non-existent ID. -type SemanticID uint - -// SystemEvent is a marker for events that have platform specific -// side-effects. SystemEvents are never matched by catch-all filters. -type SystemEvent struct { - Event event.Event -} - -// handler contains the per-handler state tracked by a [Router]. -type handler struct { - // active tracks whether the handler was active in the current - // frame. Router deletes state belonging to inactive handlers during Frame. - active bool - pointer pointerHandler - key keyHandler - // filter the handler has asked for through event handling - // in the previous frame. It is used for routing events in the - // current frame. - filter filter - // prevFilter is the filter being built in the current frame. - nextFilter filter - // processedFilter is the filters that have exhausted available events. - processedFilter filter -} - -// filter is the union of a set of [io/event.Filters]. -type filter struct { - pointer pointerFilter - focusable bool -} - -// taggedFilter is a filter for a particular tag. -type taggedFilter struct { - tag event.Tag - filter filter -} - -// stateChange represents the new state and outgoing events -// resulting from an incoming event. -type stateChange struct { - // event, if set, is the trigger for the change. - event event.Event - state inputState - events []taggedEvent -} - -// inputState represent a immutable snapshot of the state required -// to route events. -type inputState struct { - clipboardState - keyState - pointerState -} - -// taggedEvent represents an event and its target handler. -type taggedEvent struct { - event event.Event - tag event.Tag -} - -// Source returns a Source backed by this Router. -func (q *Router) Source() Source { - return Source{r: q} -} - -// Execute a command. -func (s Source) Execute(c Command) { - if !s.Enabled() { - return - } - s.r.execute(c) -} - -// Disabled returns a copy of this source that don't deliver any events. -func (s Source) Disabled() Source { - s2 := s - s2.disabled = true - return s2 -} - -// Enabled reports whether the source is enabled. Only enabled -// Sources deliver events. -func (s Source) Enabled() bool { - return s.r != nil && !s.disabled -} - -// Focused reports whether tag is focused, according to the most recent -// [key.FocusEvent] delivered. -func (s Source) Focused(tag event.Tag) bool { - if !s.Enabled() { - return false - } - return s.r.state().keyState.focus == tag -} - -// Event returns the next event that matches at least one of filters. -// If the source is disabled, no events will be reported. -func (s Source) Event(filters ...event.Filter) (event.Event, bool) { - if !s.Enabled() { - return nil, false - } - return s.r.Event(filters...) -} - -func (q *Router) Event(filters ...event.Filter) (event.Event, bool) { - // Merge filters into scratch filters. - q.scratchFilters = q.scratchFilters[:0] - q.key.scratchFilter = q.key.scratchFilter[:0] - for _, f := range filters { - var t event.Tag - switch f := f.(type) { - case key.Filter: - q.key.scratchFilter = append(q.key.scratchFilter, f) - continue - case transfer.SourceFilter: - t = f.Target - case transfer.TargetFilter: - t = f.Target - case key.FocusFilter: - t = f.Target - case pointer.Filter: - t = f.Target - } - if t == nil { - continue - } - var filter *filter - for i := range q.scratchFilters { - s := &q.scratchFilters[i] - if s.tag == t { - filter = &s.filter - break - } - } - if filter == nil { - n := len(q.scratchFilters) - if n < cap(q.scratchFilters) { - // Re-use previously allocated filter. - q.scratchFilters = q.scratchFilters[:n+1] - tf := &q.scratchFilters[n] - tf.tag = t - filter = &tf.filter - filter.Reset() - } else { - q.scratchFilters = append(q.scratchFilters, taggedFilter{tag: t}) - filter = &q.scratchFilters[n].filter - } - } - filter.Add(f) - } - for _, tf := range q.scratchFilters { - h := q.stateFor(tf.tag) - h.filter.Merge(tf.filter) - h.nextFilter.Merge(tf.filter) - } - q.key.filter = append(q.key.filter, q.key.scratchFilter...) - q.key.nextFilter = append(q.key.nextFilter, q.key.scratchFilter...) - // Deliver reset event, if any. - for _, f := range filters { - switch f := f.(type) { - case key.FocusFilter: - if f.Target == nil { - break - } - h := q.stateFor(f.Target) - if reset, ok := h.key.ResetEvent(); ok { - return reset, true - } - case pointer.Filter: - if f.Target == nil { - break - } - h := q.stateFor(f.Target) - if reset, ok := h.pointer.ResetEvent(); ok && h.filter.pointer.Matches(reset) { - return reset, true - } - } - } - for i := range q.changes { - if q.deferring && i > 0 { - break - } - change := &q.changes[i] - for j, evt := range change.events { - match := false - switch e := evt.event.(type) { - case key.Event: - match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false) - default: - for _, tf := range q.scratchFilters { - if evt.tag == tf.tag && tf.filter.Matches(evt.event) { - match = true - break - } - } - } - if match { - change.events = slices.Delete(change.events, j, j+1) - // Fast forward state to last matched. - q.collapseState(i) - return evt.event, true - } - } - } - for _, tf := range q.scratchFilters { - h := q.stateFor(tf.tag) - h.processedFilter.Merge(tf.filter) - } - return nil, false -} - -// collapseState in the interval [1;idx] into q.changes[0]. -func (q *Router) collapseState(idx int) { - if idx == 0 { - return - } - first := &q.changes[0] - first.state = q.changes[idx].state - for _, ch := range q.changes[1 : idx+1] { - first.events = append(first.events, ch.events...) - } - q.changes = append(q.changes[:1], q.changes[idx+1:]...) -} - -// Frame completes the current frame and starts a new with the -// handlers from the frame argument. Remaining events are discarded, -// unless they were deferred by a command. -func (q *Router) Frame(frame *op.Ops) { - var remaining []event.Event - if n := len(q.changes); n > 0 { - if q.deferring { - // Collect events for replay. - for _, ch := range q.changes[1:] { - remaining = append(remaining, ch.event) - } - q.changes = append(q.changes[:0], stateChange{state: q.changes[0].state}) - } else { - // Collapse state. - state := q.changes[n-1].state - q.changes = append(q.changes[:0], stateChange{state: state}) - } - } - for _, rc := range q.transfers { - if rc != nil { - rc.Close() - } - } - q.transfers = nil - q.deferring = false - for _, h := range q.handlers { - h.filter, h.nextFilter = h.nextFilter, h.filter - h.nextFilter.Reset() - h.processedFilter.Reset() - h.pointer.Reset() - h.key.Reset() - } - q.key.filter, q.key.nextFilter = q.key.nextFilter, q.key.filter - q.key.nextFilter = q.key.nextFilter[:0] - var ops *ops.Ops - if frame != nil { - ops = &frame.Internal - } - q.reader.Reset(ops) - q.collect() - for k, h := range q.handlers { - if !h.active { - delete(q.handlers, k) - } else { - h.active = false - } - } - q.executeCommands() - q.Queue(remaining...) - st := q.lastState() - pst, evts := q.pointer.queue.Frame(q.handlers, st.pointerState) - st.pointerState = pst - st.keyState = q.key.queue.Frame(q.handlers, q.lastState().keyState) - q.changeState(nil, st, evts) - - // Collapse state and events. - q.collapseState(len(q.changes) - 1) -} - -// Queue events to be routed. -func (q *Router) Queue(events ...event.Event) { - for _, e := range events { - se, system := e.(SystemEvent) - if system { - e = se.Event - } - q.processEvent(e, system) - } -} - -func (f *filter) Add(flt event.Filter) { - switch flt := flt.(type) { - case key.FocusFilter: - f.focusable = true - case pointer.Filter: - f.pointer.Add(flt) - case transfer.SourceFilter, transfer.TargetFilter: - f.pointer.Add(flt) - } -} - -// Merge f2 into f. -func (f *filter) Merge(f2 filter) { - f.focusable = f.focusable || f2.focusable - f.pointer.Merge(f2.pointer) -} - -func (f *filter) Matches(e event.Event) bool { - switch e.(type) { - case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent: - return f.focusable - default: - return f.pointer.Matches(e) - } -} - -func (f *filter) Reset() { - *f = filter{ - pointer: pointerFilter{ - sourceMimes: f.pointer.sourceMimes[:0], - targetMimes: f.pointer.targetMimes[:0], - }, - } -} - -func (q *Router) processEvent(e event.Event, system bool) { - state := q.lastState() - switch e := e.(type) { - case pointer.Event: - pstate, evts := q.pointer.queue.Push(q.handlers, state.pointerState, e) - state.pointerState = pstate - q.changeState(e, state, evts) - case key.Event: - var evts []taggedEvent - if q.key.filter.Matches(state.keyState.focus, e, system) { - evts = append(evts, taggedEvent{event: e}) - } - q.changeState(e, state, evts) - case key.SnippetEvent: - // Expand existing, overlapping snippet. - if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) { - if e.Start > r.Start { - e.Start = r.Start - } - if e.End < r.End { - e.End = r.End - } - } - var evts []taggedEvent - if f := state.focus; f != nil { - evts = append(evts, taggedEvent{tag: f, event: e}) - } - q.changeState(e, state, evts) - case key.EditEvent, key.FocusEvent, key.SelectionEvent: - var evts []taggedEvent - if f := state.focus; f != nil { - evts = append(evts, taggedEvent{tag: f, event: e}) - } - q.changeState(e, state, evts) - case transfer.DataEvent: - cstate, evts := q.cqueue.Push(state.clipboardState, e) - state.clipboardState = cstate - q.changeState(e, state, evts) - default: - panic("unknown event type") - } -} - -func (q *Router) execute(c Command) { - // The command can be executed immediately if event delivery is not frozen, and - // no event receiver has completed their event handling. - if !q.deferring { - ch := q.executeCommand(c) - immediate := true - for _, e := range ch.events { - h, ok := q.handlers[e.tag] - immediate = immediate && (!ok || !h.processedFilter.Matches(e.event)) - } - if immediate { - // Hold on to the remaining events for state replay. - var evts []event.Event - for _, ch := range q.changes { - if ch.event != nil { - evts = append(evts, ch.event) - } - } - if len(q.changes) > 1 { - q.changes = q.changes[:1] - } - q.changeState(nil, ch.state, ch.events) - q.Queue(evts...) - return - } - } - q.deferring = true - q.commands = append(q.commands, c) -} - -func (q *Router) state() inputState { - if len(q.changes) > 0 { - return q.changes[0].state - } - return inputState{} -} - -func (q *Router) lastState() inputState { - if n := len(q.changes); n > 0 { - return q.changes[n-1].state - } - return inputState{} -} - -func (q *Router) executeCommands() { - for _, c := range q.commands { - ch := q.executeCommand(c) - q.changeState(nil, ch.state, ch.events) - } - q.commands = nil -} - -// executeCommand the command and return the resulting state change along with the -// tag the state change depended on, if any. -func (q *Router) executeCommand(c Command) stateChange { - state := q.state() - var evts []taggedEvent - switch req := c.(type) { - case key.SelectionCmd: - state.keyState = q.key.queue.setSelection(state.keyState, req) - case key.FocusCmd: - state.keyState, evts = q.key.queue.Focus(q.handlers, state.keyState, req.Tag) - case key.SoftKeyboardCmd: - state.keyState = state.keyState.softKeyboard(req.Show) - case key.SnippetCmd: - state.keyState = q.key.queue.setSnippet(state.keyState, req) - case transfer.OfferCmd: - state.pointerState, evts = q.pointer.queue.offerData(q.handlers, state.pointerState, req) - case clipboard.WriteCmd: - q.cqueue.ProcessWriteClipboard(req) - case clipboard.ReadCmd: - state.clipboardState = q.cqueue.ProcessReadClipboard(state.clipboardState, req.Tag) - case pointer.GrabCmd: - state.pointerState, evts = q.pointer.queue.grab(state.pointerState, req) - case op.InvalidateCmd: - if !q.wakeup || req.At.Before(q.wakeupTime) { - q.wakeup = true - q.wakeupTime = req.At - } - } - return stateChange{state: state, events: evts} -} - -func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent) { - // Wrap pointer.DataEvent.Open functions to detect them not being called. - for i := range evts { - e := &evts[i] - if de, ok := e.event.(transfer.DataEvent); ok { - transferIdx := len(q.transfers) - data := de.Open() - q.transfers = append(q.transfers, data) - de.Open = func() io.ReadCloser { - q.transfers[transferIdx] = nil - return data - } - e.event = de - } - } - // Initialize the first change to contain the current state - // and events that are bound for the current frame. - if len(q.changes) == 0 { - q.changes = append(q.changes, stateChange{}) - } - if e != nil && len(evts) > 0 { - // An event triggered events bound for user receivers. Add a state change to be - // able to redo the change in case of a command execution. - q.changes = append(q.changes, stateChange{event: e, state: state, events: evts}) - } else { - // Otherwise, merge with previous change. - prev := &q.changes[len(q.changes)-1] - prev.state = state - prev.events = append(prev.events, evts...) - } -} - -func rangeOverlaps(r1, r2 key.Range) bool { - r1 = rangeNorm(r1) - r2 = rangeNorm(r2) - return r1.Start <= r2.Start && r2.Start < r1.End || - r1.Start <= r2.End && r2.End < r1.End -} - -func rangeNorm(r key.Range) key.Range { - if r.End < r.Start { - r.End, r.Start = r.Start, r.End - } - return r -} - -func (q *Router) MoveFocus(dir key.FocusDirection) { - state := q.lastState() - kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir) - state.keyState = kstate - q.changeState(nil, state, evts) -} - -// RevealFocus scrolls the current focus (if any) into viewport -// if there are scrollable parent handlers. -func (q *Router) RevealFocus(viewport image.Rectangle) { - state := q.lastState() - focus := state.focus - if focus == nil { - return - } - kh := &q.handlers[focus].key - bounds := q.key.queue.BoundsFor(kh) - area := q.key.queue.AreaFor(kh) - viewport = q.pointer.queue.ClipFor(area, viewport) - - topleft := bounds.Min.Sub(viewport.Min) - topleft = maxPoint(topleft, bounds.Max.Sub(viewport.Max)) - topleft = minPoint(image.Pt(0, 0), topleft) - bottomright := bounds.Max.Sub(viewport.Max) - bottomright = minPoint(bottomright, bounds.Min.Sub(viewport.Min)) - bottomright = maxPoint(image.Pt(0, 0), bottomright) - s := topleft - if s.X == 0 { - s.X = bottomright.X - } - if s.Y == 0 { - s.Y = bottomright.Y - } - q.ScrollFocus(s) -} - -// ScrollFocus scrolls the focused widget, if any, by dist. -func (q *Router) ScrollFocus(dist image.Point) { - state := q.lastState() - focus := state.focus - if focus == nil { - return - } - kh := &q.handlers[focus].key - area := q.key.queue.AreaFor(kh) - q.changeState(nil, q.lastState(), q.pointer.queue.Deliver(q.handlers, area, pointer.Event{ - Kind: pointer.Scroll, - Source: pointer.Touch, - Scroll: f32internal.FPt(dist), - })) -} - -func maxPoint(p1, p2 image.Point) image.Point { - m := p1 - if p2.X > m.X { - m.X = p2.X - } - if p2.Y > m.Y { - m.Y = p2.Y - } - return m -} - -func minPoint(p1, p2 image.Point) image.Point { - m := p1 - if p2.X < m.X { - m.X = p2.X - } - if p2.Y < m.Y { - m.Y = p2.Y - } - return m -} - -func (q *Router) ActionAt(p f32.Point) (system.Action, bool) { - return q.pointer.queue.ActionAt(p) -} - -func (q *Router) ClickFocus() { - focus := q.lastState().focus - if focus == nil { - return - } - kh := &q.handlers[focus].key - bounds := q.key.queue.BoundsFor(kh) - center := bounds.Max.Add(bounds.Min).Div(2) - e := pointer.Event{ - Position: f32.Pt(float32(center.X), float32(center.Y)), - Source: pointer.Touch, - } - area := q.key.queue.AreaFor(kh) - e.Kind = pointer.Press - state := q.lastState() - q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e)) - e.Kind = pointer.Release - q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e)) -} - -// TextInputState returns the input state from the most recent -// call to Frame. -func (q *Router) TextInputState() TextInputState { - state := q.state() - kstate, s := state.InputState() - state.keyState = kstate - q.changeState(nil, state, nil) - return s -} - -// TextInputHint returns the input mode from the most recent key.InputOp. -func (q *Router) TextInputHint() (key.InputHint, bool) { - return q.key.queue.InputHint(q.handlers, q.state().keyState) -} - -// WriteClipboard returns the most recent content to be copied -// to the clipboard, if any. -func (q *Router) WriteClipboard() (mime string, content []byte, ok bool) { - return q.cqueue.WriteClipboard() -} - -// ClipboardRequested reports if any new handler is waiting -// to read the clipboard. -func (q *Router) ClipboardRequested() bool { - return q.cqueue.ClipboardRequested(q.lastState().clipboardState) -} - -// Cursor returns the last cursor set. -func (q *Router) Cursor() pointer.Cursor { - return q.state().cursor -} - -// SemanticAt returns the first semantic description under pos, if any. -func (q *Router) SemanticAt(pos f32.Point) (SemanticID, bool) { - return q.pointer.queue.SemanticAt(pos) -} - -// AppendSemantics appends the semantic tree to nodes, and returns the result. -// The root node is the first added. -func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode { - q.pointer.collector.q = &q.pointer.queue - q.pointer.collector.ensureRoot() - return q.pointer.queue.AppendSemantics(nodes) -} - -// EditorState returns the editor state for the focused handler, or the -// zero value if there is none. -func (q *Router) EditorState() EditorState { - return q.key.queue.editorState(q.handlers, q.state().keyState) -} - -func (q *Router) stateFor(tag event.Tag) *handler { - if tag == nil { - panic("internal error: nil tag") - } - s, ok := q.handlers[tag] - if !ok { - s = new(handler) - if q.handlers == nil { - q.handlers = make(map[event.Tag]*handler) - } - q.handlers[tag] = s - } - s.active = true - return s -} - -func (q *Router) collect() { - q.transStack = q.transStack[:0] - pc := &q.pointer.collector - pc.q = &q.pointer.queue - pc.Reset() - kq := &q.key.queue - q.key.queue.Reset() - t := f32.AffineId() - for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { - switch ops.OpType(encOp.Data[0]) { - case ops.TypeSave: - id := ops.DecodeSave(encOp.Data) - if extra := id - len(q.savedTrans) + 1; extra > 0 { - for range extra { - q.savedTrans = append(q.savedTrans, f32.AffineId()) - } - } - q.savedTrans[id] = t - case ops.TypeLoad: - id := ops.DecodeLoad(encOp.Data) - t = q.savedTrans[id] - pc.resetState() - pc.setTrans(t) - - case ops.TypeClip: - var op ops.ClipOp - op.Decode(encOp.Data) - pc.clip(op) - case ops.TypePopClip: - pc.popArea() - case ops.TypeTransform: - t2, push := ops.DecodeTransform(encOp.Data) - if push { - q.transStack = append(q.transStack, t) - } - t = t.Mul(t2) - pc.setTrans(t) - case ops.TypePopTransform: - n := len(q.transStack) - t = q.transStack[n-1] - q.transStack = q.transStack[:n-1] - pc.setTrans(t) - - case ops.TypeInput: - tag := encOp.Refs[0].(event.Tag) - s := q.stateFor(tag) - pc.inputOp(tag, &s.pointer) - a := pc.currentArea() - b := pc.currentAreaBounds() - if s.filter.focusable { - kq.inputOp(tag, &s.key, t, a, b) - } - - // Pointer ops. - case ops.TypePass: - pc.pass() - case ops.TypePopPass: - pc.popPass() - case ops.TypeCursor: - name := pointer.Cursor(encOp.Data[1]) - pc.cursor(name) - case ops.TypeActionInput: - act := system.Action(encOp.Data[1]) - pc.actionInputOp(act) - case ops.TypeKeyInputHint: - op := key.InputHintOp{ - Tag: encOp.Refs[0].(event.Tag), - Hint: key.InputHint(encOp.Data[1]), - } - s := q.stateFor(op.Tag) - s.key.inputHint(op.Hint) - - // Semantic ops. - case ops.TypeSemanticLabel: - lbl := *encOp.Refs[0].(*string) - pc.semanticLabel(lbl) - case ops.TypeSemanticDesc: - desc := *encOp.Refs[0].(*string) - pc.semanticDesc(desc) - case ops.TypeSemanticClass: - class := semantic.ClassOp(encOp.Data[1]) - pc.semanticClass(class) - case ops.TypeSemanticSelected: - if encOp.Data[1] != 0 { - pc.semanticSelected(true) - } else { - pc.semanticSelected(false) - } - case ops.TypeSemanticEnabled: - if encOp.Data[1] != 0 { - pc.semanticEnabled(true) - } else { - pc.semanticEnabled(false) - } - } - } -} - -// WakeupTime returns the most recent time for doing another frame, -// as determined from the last call to Frame. -func (q *Router) WakeupTime() (time.Time, bool) { - t, w := q.wakeupTime, q.wakeup - q.wakeup = false - // Pending events always trigger wakeups. - if len(q.changes) > 1 || len(q.changes) == 1 && len(q.changes[0].events) > 0 { - t, w = time.Time{}, true - } - return t, w -} - -func (s SemanticGestures) String() string { - var gestures []string - if s&ClickGesture != 0 { - gestures = append(gestures, "Click") - } - return strings.Join(gestures, ",") -} - -func (SystemEvent) ImplementsEvent() {} diff --git a/gio/io/input/router_test.go b/gio/io/input/router_test.go deleted file mode 100644 index d58cf03..0000000 --- a/gio/io/input/router_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package input - -import ( - "testing" - - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -func TestNoFilterAllocs(t *testing.T) { - b := testing.Benchmark(func(b *testing.B) { - var r Router - s := r.Source() - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - s.Event(pointer.Filter{}) - } - }) - if allocs := b.AllocsPerOp(); allocs != 0 { - t.Fatalf("expected 0 AllocsPerOp, got %d", allocs) - } -} - -func TestRouterWakeup(t *testing.T) { - r := new(Router) - r.Source().Execute(op.InvalidateCmd{}) - r.Frame(new(op.Ops)) - if _, wake := r.WakeupTime(); !wake { - t.Errorf("InvalidateCmd did not trigger a redraw") - } -} diff --git a/gio/io/input/semantic_test.go b/gio/io/input/semantic_test.go deleted file mode 100644 index b4f53af..0000000 --- a/gio/io/input/semantic_test.go +++ /dev/null @@ -1,135 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package input - -import ( - "fmt" - "image" - "reflect" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -func TestEmptySemantics(t *testing.T) { - var r Router - tree := r.AppendSemantics(nil) - if len(tree) != 1 { - t.Errorf("expected 1 semantic node for empty tree, got %d", len(tree)) - } -} - -func TestSemanticTree(t *testing.T) { - var ( - ops op.Ops - r Router - ) - t1 := clip.Rect(image.Rect(0, 0, 75, 75)).Push(&ops) - semantic.DescriptionOp("child1").Add(&ops) - t1.Pop() - t2 := clip.Rect(image.Rect(25, 25, 100, 100)).Push(&ops) - semantic.DescriptionOp("child2").Add(&ops) - t2.Pop() - r.Frame(&ops) - tests := []struct { - x, y float32 - desc string - }{ - {24, 24, "child1"}, - {50, 50, "child2"}, - {100, 100, ""}, - } - tree := r.AppendSemantics(nil) - verifyTree(t, 0, tree[0]) - for _, test := range tests { - p := f32.Pt(test.x, test.y) - id, found := r.SemanticAt(p) - if !found { - t.Errorf("no semantic node at %v", p) - } - n, found := lookupNode(tree, id) - if !found { - t.Errorf("no id %d in semantic tree", id) - } - if got := n.Desc.Description; got != test.desc { - t.Errorf("got semantic description %s at %v, expected %s", got, p, test.desc) - } - } - - // Verify stable IDs. - r.Frame(&ops) - tree2 := r.AppendSemantics(nil) - if !reflect.DeepEqual(tree, tree2) { - fmt.Println("First tree:") - printTree(0, tree[0]) - fmt.Println("Second tree:") - printTree(0, tree2[0]) - t.Error("same semantic description lead to differing trees") - } -} - -func TestSemanticDescription(t *testing.T) { - var ops op.Ops - - h := new(int) - event.Op(&ops, h) - semantic.DescriptionOp("description").Add(&ops) - semantic.LabelOp("label").Add(&ops) - semantic.Button.Add(&ops) - semantic.EnabledOp(false).Add(&ops) - semantic.SelectedOp(true).Add(&ops) - var r Router - events(&r, -1, pointer.Filter{ - Target: h, - Kinds: pointer.Press | pointer.Release, - }) - r.Frame(&ops) - tree := r.AppendSemantics(nil) - got := tree[0].Desc - exp := SemanticDesc{ - Class: 1, - Description: "description", - Label: "label", - Selected: true, - Disabled: true, - Gestures: ClickGesture, - Bounds: image.Rectangle{Min: image.Point{X: -1e+06, Y: -1e+06}, Max: image.Point{X: 1e+06, Y: 1e+06}}, - } - if got != exp { - t.Errorf("semantic description mismatch:\nGot: %+v\nWant: %+v", got, exp) - } -} - -func lookupNode(tree []SemanticNode, id SemanticID) (SemanticNode, bool) { - for _, n := range tree { - if id == n.ID { - return n, true - } - } - return SemanticNode{}, false -} - -func verifyTree(t *testing.T, parent SemanticID, n SemanticNode) { - t.Helper() - if n.ParentID != parent { - t.Errorf("node %d: got parent %d, want %d", n.ID, n.ParentID, parent) - } - for _, c := range n.Children { - verifyTree(t, n.ID, c) - } -} - -func printTree(indent int, n SemanticNode) { - for range indent { - fmt.Print("\t") - } - fmt.Printf("%d: %+v\n", n.ID, n.Desc) - for _, c := range n.Children { - printTree(indent+1, c) - } -} diff --git a/gio/io/key/key.go b/gio/io/key/key.go deleted file mode 100644 index 9b3f8fd..0000000 --- a/gio/io/key/key.go +++ /dev/null @@ -1,289 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package key implements key and text events and operations. -package key - -import ( - "strings" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Filter matches any [Event] that matches the parameters. -type Filter struct { - // Focus is the tag that must be focused for the filter to match. It has no effect - // if it is nil. - Focus event.Tag - // Required is the set of modifiers that must be included in events matched. - Required Modifiers - // Optional is the set of modifiers that may be included in events matched. - Optional Modifiers - // Name of the key to be matched. As a special case, the empty - // Name matches every key not matched by any other filter. - Name Name -} - -// InputHintOp describes the type of text expected by a tag. -type InputHintOp struct { - Tag event.Tag - Hint InputHint -} - -// SoftKeyboardCmd shows or hides the on-screen keyboard, if available. -type SoftKeyboardCmd struct { - Show bool -} - -// SelectionCmd updates the selection for an input handler. -type SelectionCmd struct { - Tag event.Tag - Range - Caret -} - -// SnippetCmd updates the content snippet for an input handler. -type SnippetCmd struct { - Tag event.Tag - Snippet -} - -// Range represents a range of text, such as an editor's selection. -// Start and End are in runes. -type Range struct { - Start int - End int -} - -// Snippet represents a snippet of text content used for communicating between -// an editor and an input method. -type Snippet struct { - Range - Text string -} - -// Caret represents the position of a caret. -type Caret struct { - // Pos is the intersection point of the caret and its baseline. - Pos f32.Point - // Ascent is the length of the caret above its baseline. - Ascent float32 - // Descent is the length of the caret below its baseline. - Descent float32 -} - -// SelectionEvent is generated when an input method changes the selection. -type SelectionEvent Range - -// SnippetEvent is generated when the snippet range is updated by an -// input method. -type SnippetEvent Range - -// A FocusEvent is generated when a handler gains or loses -// focus. -type FocusEvent struct { - Focus bool -} - -// An Event is generated when a key is pressed. For text input -// use EditEvent. -type Event struct { - // Name of the key. - Name Name - // Modifiers is the set of active modifiers when the key was pressed. - Modifiers Modifiers - // State is the state of the key when the event was fired. - State State -} - -// An EditEvent requests an edit by an input method. -type EditEvent struct { - // Range specifies the range to replace with Text. - Range Range - Text string -} - -// FocusFilter matches any [FocusEvent], [EditEvent], [SnippetEvent], -// or [SelectionEvent] with the specified target. -type FocusFilter struct { - // Target is a tag specified in a previous event.Op. - Target event.Tag -} - -// InputHint changes the on-screen-keyboard type. That hints the -// type of data that might be entered by the user. -type InputHint uint8 - -const ( - // HintAny hints that any input is expected. - HintAny InputHint = iota - // HintText hints that text input is expected. It may activate auto-correction and suggestions. - HintText - // HintNumeric hints that numeric input is expected. It may activate shortcuts for 0-9, "." and ",". - HintNumeric - // HintEmail hints that email input is expected. It may activate shortcuts for common email characters, such as "@" and ".com". - HintEmail - // HintURL hints that URL input is expected. It may activate shortcuts for common URL fragments such as "/" and ".com". - HintURL - // HintTelephone hints that telephone number input is expected. It may activate shortcuts for 0-9, "#" and "*". - HintTelephone - // HintPassword hints that password input is expected. It may disable autocorrection and enable password autofill. - HintPassword -) - -// State is the state of a key during an event. -type State uint8 - -const ( - // Press is the state of a pressed key. - Press State = iota - // Release is the state of a key that has been released. - // - // Note: release events are only implemented on the following platforms: - // macOS, Linux, Windows, WebAssembly. - Release -) - -// Modifiers -type Modifiers uint32 - -const ( - // ModCtrl is the ctrl modifier key. - ModCtrl Modifiers = 1 << iota - // ModCommand is the command modifier key - // found on Apple keyboards. - ModCommand - // ModShift is the shift modifier key. - ModShift - // ModAlt is the alt modifier key, or the option - // key on Apple keyboards. - ModAlt - // ModSuper is the "logo" modifier key, often - // represented by a Windows logo. - ModSuper -) - -// Name is the identifier for a keyboard key. -// -// For letters, the upper case form is used, via unicode.ToUpper. -// The shift modifier is taken into account, all other -// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1" -// combinations both give the Name "!" with the US keyboard layout. -type Name string - -const ( - // Names for special keys. - NameLeftArrow Name = "←" - NameRightArrow Name = "→" - NameUpArrow Name = "↑" - NameDownArrow Name = "↓" - NameReturn Name = "⏎" - NameEnter Name = "⌤" - NameEscape Name = "⎋" - NameHome Name = "⇱" - NameEnd Name = "⇲" - NameDeleteBackward Name = "⌫" - NameDeleteForward Name = "⌦" - NamePageUp Name = "⇞" - NamePageDown Name = "⇟" - NameTab Name = "Tab" - NameSpace Name = "Space" - NameCtrl Name = "Ctrl" - NameShift Name = "Shift" - NameAlt Name = "Alt" - NameSuper Name = "Super" - NameCommand Name = "⌘" - NameF1 Name = "F1" - NameF2 Name = "F2" - NameF3 Name = "F3" - NameF4 Name = "F4" - NameF5 Name = "F5" - NameF6 Name = "F6" - NameF7 Name = "F7" - NameF8 Name = "F8" - NameF9 Name = "F9" - NameF10 Name = "F10" - NameF11 Name = "F11" - NameF12 Name = "F12" - NameBack Name = "Back" -) - -type FocusDirection int - -const ( - FocusRight FocusDirection = iota - FocusLeft - FocusUp - FocusDown - FocusForward - FocusBackward -) - -// Contain reports whether m contains all modifiers -// in m2. -func (m Modifiers) Contain(m2 Modifiers) bool { - return m&m2 == m2 -} - -// FocusCmd requests to set or clear the keyboard focus. -type FocusCmd struct { - // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag - // has no [event.Op] references. - Tag event.Tag -} - -func (h InputHintOp) Add(o *op.Ops) { - if h.Tag == nil { - panic("Tag must be non-nil") - } - data := ops.Write1(&o.Internal, ops.TypeKeyInputHintLen, h.Tag) - data[0] = byte(ops.TypeKeyInputHint) - data[1] = byte(h.Hint) -} - -func (EditEvent) ImplementsEvent() {} -func (Event) ImplementsEvent() {} -func (FocusEvent) ImplementsEvent() {} -func (SnippetEvent) ImplementsEvent() {} -func (SelectionEvent) ImplementsEvent() {} - -func (FocusCmd) ImplementsCommand() {} -func (SoftKeyboardCmd) ImplementsCommand() {} -func (SelectionCmd) ImplementsCommand() {} -func (SnippetCmd) ImplementsCommand() {} - -func (Filter) ImplementsFilter() {} -func (FocusFilter) ImplementsFilter() {} - -func (m Modifiers) String() string { - var strs []string - if m.Contain(ModCtrl) { - strs = append(strs, string(NameCtrl)) - } - if m.Contain(ModCommand) { - strs = append(strs, string(NameCommand)) - } - if m.Contain(ModShift) { - strs = append(strs, string(NameShift)) - } - if m.Contain(ModAlt) { - strs = append(strs, string(NameAlt)) - } - if m.Contain(ModSuper) { - strs = append(strs, string(NameSuper)) - } - return strings.Join(strs, "-") -} - -func (s State) String() string { - switch s { - case Press: - return "Press" - case Release: - return "Release" - default: - panic("invalid State") - } -} diff --git a/gio/io/key/mod.go b/gio/io/key/mod.go deleted file mode 100644 index 3cf7c2d..0000000 --- a/gio/io/key/mod.go +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build !darwin -// +build !darwin - -package key - -// ModShortcut is the platform's shortcut modifier, usually the ctrl -// modifier. On Apple platforms it is the cmd key. -const ModShortcut = ModCtrl - -// ModShortcutAlt is the platform's alternative shortcut modifier, -// usually the ctrl modifier. On Apple platforms it is the alt modifier. -const ModShortcutAlt = ModCtrl diff --git a/gio/io/key/mod_darwin.go b/gio/io/key/mod_darwin.go deleted file mode 100644 index 929b392..0000000 --- a/gio/io/key/mod_darwin.go +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package key - -// ModShortcut is the platform's shortcut modifier, usually the ctrl -// modifier. On Apple platforms it is the cmd key. -const ModShortcut = ModCommand - -// ModShortcut is the platform's alternative shortcut modifier, -// usually the ctrl modifier. On Apple platforms it is the alt modifier. -const ModShortcutAlt = ModAlt diff --git a/gio/io/pointer/doc.go b/gio/io/pointer/doc.go deleted file mode 100644 index ad2993d..0000000 --- a/gio/io/pointer/doc.go +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package pointer implements pointer events and operations. -A pointer is either a mouse controlled cursor or a touch -object such as a finger. - -The [event.Op] operation is used to declare a handler ready for pointer -events. - -# Hit areas - -Clip operations from package [op/clip] are used for specifying -hit areas where handlers may receive events. - -For example, to set up a handler with a rectangular hit area: - - r := image.Rectangle{...} - area := clip.Rect(r).Push(ops) - event.Op{Tag: h}.Add(ops) - area.Pop() - -Note that hit areas behave similar to painting: the effective area of a stack -of multiple area operations is the intersection of the areas. - -BUG: Clip operations other than clip.Rect and clip.Ellipse are approximated -with their bounding boxes. - -# Matching events - -Areas form an implicit tree, with input handlers as leaves. The children of -an area is every area and handler added between its Push and corresponding Pop. - -For example: - - ops := new(op.Ops) - var h1, h2 *Handler - - area := clip.Rect(...).Push(ops) - event.Op(Ops, h1) - area.Pop() - - area := clip.Rect(...).Push(ops) - event.Op(Ops, h2) - area.Pop() - -implies a tree of two inner nodes, each with one pointer handler attached. - -The matching proceeds as follows. - -First, the foremost area that contains the event is found. Only areas whose -parent areas all contain the event is considered. - -Then, every handler attached to the area is matched with the event. - -If all attached handlers are marked pass-through or if no handlers are -attached, the matching repeats with the next foremost (sibling) area. Otherwise -the matching repeats with the parent area. - -In the example above, all events will go to h2 because it and h1 are siblings -and none are pass-through. - -# Pass-through - -The PassOp operations controls the pass-through setting. All handlers added -inside one or more PassOp scopes are marked pass-through. - -Pass-through is useful for overlay widgets. Consider a hidden side drawer: when -the user touches the side, both the (transparent) drawer handle and the -interface below should receive pointer events. This effect is achieved by -marking the drawer handle pass-through. - -# Disambiguation - -When more than one handler matches a pointer event, the event queue -follows a set of rules for distributing the event. - -As long as the pointer has not received a Press event, all -matching handlers receive all events. - -When a pointer is pressed, the set of matching handlers is -recorded. The set is not updated according to the pointer position -and hit areas. Rather, handlers stay in the matching set until they -no longer appear in a InputOp or when another handler in the set -grabs the pointer. - -A handler can exclude all other handler from its matching sets -by setting the Grab flag in its InputOp. The Grab flag is sticky -and stays in effect until the handler no longer appears in any -matching sets. - -The losing handlers are notified by a Cancel event. - -For multiple grabbing handlers, the foremost handler wins. - -# Priorities - -Handlers know their position in a matching set of a pointer through -event priorities. The Shared priority is for matching sets with -multiple handlers; the Grabbed priority indicate exclusive access. - -Priorities are useful for deferred gesture matching. - -Consider a scrollable list of clickable elements. When the user touches an -element, it is unknown whether the gesture is a click on the element -or a drag (scroll) of the list. While the click handler might light up -the element in anticipation of a click, the scrolling handler does not -scroll on finger movements with lower than Grabbed priority. - -Should the user release the finger, the click handler registers a click. - -However, if the finger moves beyond a threshold, the scrolling handler -determines that the gesture is a drag and sets its Grab flag. The -click handler receives a Cancel (removing the highlight) and further -movements for the scroll handler has priority Grabbed, scrolling the -list. -*/ -package pointer diff --git a/gio/io/pointer/pointer.go b/gio/io/pointer/pointer.go deleted file mode 100644 index 02e4e73..0000000 --- a/gio/io/pointer/pointer.go +++ /dev/null @@ -1,407 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package pointer - -import ( - "strings" - "time" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Event is a pointer event. -type Event struct { - Kind Kind - Source Source - // PointerID is the id for the pointer and can be used - // to track a particular pointer from Press to - // Release. - PointerID ID - // Priority is the priority of the receiving handler - // for this event. - Priority Priority - // Time is when the event was received. The - // timestamp is relative to an undefined base. - Time time.Duration - // Buttons are the set of pressed mouse buttons for this event. - Buttons Buttons - // Position is the coordinates of the event in the local coordinate - // system of the receiving tag. The transformation from global window - // coordinates to local coordinates is performed by the inverse of - // the effective transformation of the tag. - Position f32.Point - // Scroll is the scroll amount, if any. - Scroll f32.Point - // Modifiers is the set of active modifiers when - // the mouse button was pressed. - Modifiers key.Modifiers -} - -// PassOp sets the pass-through mode. InputOps added while the pass-through -// mode is set don't block events to siblings. -type PassOp struct{} - -// PassStack represents a PassOp on the pass stack. -type PassStack struct { - ops *ops.Ops - id ops.StackID - macroID uint32 -} - -// Filter matches every [Event] that target the Tag and whose kind is -// included in Kinds. Note that only tags specified in [event.Op] can -// be targeted by pointer events. -type Filter struct { - Target event.Tag - // Kinds is a bitwise-or of event types to match. - Kinds Kind - // ScrollX and ScrollY constrain the range of scrolling events delivered - // to Target. Specifically, any Event e delivered to Tag will satisfy - // - // ScrollX.Min <= e.Scroll.X <= ScrollX.Max (horizontal axis) - // ScrollY.Min <= e.Scroll.Y <= ScrollY.Max (vertical axis) - ScrollX ScrollRange - ScrollY ScrollRange -} - -// ScrollRange describes the range of scrolling distances in an -// axis. -type ScrollRange struct { - Min, Max int -} - -// GrabCmd requests a pointer grab on the pointer identified by ID. -type GrabCmd struct { - Tag event.Tag - ID ID -} - -type ID uint16 - -// Kind of an Event. -type Kind uint - -// Priority of an Event. -type Priority uint8 - -// Source of an Event. -type Source uint8 - -// Buttons is a set of mouse buttons -type Buttons uint8 - -// Cursor denotes a pre-defined cursor shape. Its Add method adds an -// operation that sets the cursor shape for the current clip area. -type Cursor byte - -// The cursors correspond to CSS pointer naming. -const ( - // CursorDefault is the default cursor. - CursorDefault Cursor = iota - // CursorNone hides the cursor. To show it again, use any other cursor. - CursorNone - // CursorText is for selecting and inserting text. - CursorText - // CursorVerticalText is for selecting and inserting vertical text. - CursorVerticalText - // CursorPointer is for a link. - // Usually displayed as a pointing hand. - CursorPointer - // CursorCrosshair is for a precise location. - CursorCrosshair - // CursorAllScroll is for indicating scrolling in all directions. - // Usually displayed as arrows to all four directions. - CursorAllScroll - // CursorColResize is for vertical resize. - // Usually displayed as a vertical bar with arrows pointing east and west. - CursorColResize - // CursorRowResize is for horizontal resize. - // Usually displayed as a horizontal bar with arrows pointing north and south. - CursorRowResize - // CursorGrab is for content that can be grabbed (dragged to be moved). - // Usually displayed as an open hand. - CursorGrab - // CursorGrabbing is for content that is being grabbed (dragged to be moved). - // Usually displayed as a closed hand. - CursorGrabbing - // CursorNotAllowed is shown when the request action cannot be carried out. - // Usually displayed as a circle with a line through. - CursorNotAllowed - // CursorWait is shown when the program is busy and user cannot interact. - // Usually displayed as a hourglass or the system equivalent. - CursorWait - // CursorProgress is shown when the program is busy, but the user can still interact. - // Usually displayed as a default cursor with a hourglass. - CursorProgress - // CursorNorthWestResize is for top-left corner resizing. - // Usually displayed as an arrow towards north-west. - CursorNorthWestResize - // CursorNorthEastResize is for top-right corner resizing. - // Usually displayed as an arrow towards north-east. - CursorNorthEastResize - // CursorSouthWestResize is for bottom-left corner resizing. - // Usually displayed as an arrow towards south-west. - CursorSouthWestResize - // CursorSouthEastResize is for bottom-right corner resizing. - // Usually displayed as an arrow towards south-east. - CursorSouthEastResize - // CursorNorthSouth is for top-bottom resizing. - // Usually displayed as a bi-directional arrow towards north-south. - CursorNorthSouthResize - // CursorEastWestResize is for left-right resizing. - // Usually displayed as a bi-directional arrow towards east-west. - CursorEastWestResize - // CursorWestResize is for left resizing. - // Usually displayed as an arrow towards west. - CursorWestResize - // CursorEastResize is for right resizing. - // Usually displayed as an arrow towards east. - CursorEastResize - // CursorNorthResize is for top resizing. - // Usually displayed as an arrow towards north. - CursorNorthResize - // CursorSouthResize is for bottom resizing. - // Usually displayed as an arrow towards south. - CursorSouthResize - // CursorNorthEastSouthWestResize is for top-right to bottom-left diagonal resizing. - // Usually displayed as a double ended arrow on the corresponding diagonal. - CursorNorthEastSouthWestResize - // CursorNorthWestSouthEastResize is for top-left to bottom-right diagonal resizing. - // Usually displayed as a double ended arrow on the corresponding diagonal. - CursorNorthWestSouthEastResize -) - -const ( - // A Cancel event is generated when the current gesture is - // interrupted by other handlers or the system. - Cancel Kind = 1 << iota - // Press of a pointer. - Press - // Release of a pointer. - Release - // Move of a pointer. - Move - // Drag of a pointer. - Drag - // Pointer enters an area watching for pointer input - Enter - // Pointer leaves an area watching for pointer input - Leave - // Scroll of a pointer. - Scroll -) - -const ( - // Mouse generated event. - Mouse Source = iota - // Touch generated event. - Touch -) - -const ( - // Shared priority is for handlers that - // are part of a matching set larger than 1. - Shared Priority = iota - // Grabbed is used for matching sets of size 1. - Grabbed -) - -const ( - // ButtonPrimary is the primary button, usually the left button for a - // right-handed user. - ButtonPrimary Buttons = 1 << iota - // ButtonSecondary is the secondary button, usually the right button for a - // right-handed user. - ButtonSecondary - // ButtonTertiary is the tertiary button, usually the middle button. - ButtonTertiary - // ButtonQuaternary is the fourth button, usually used for browser - // navigation (backward) - ButtonQuaternary - // ButtonQuinary is the fifth button, usually used for browser - // navigation (forward) - ButtonQuinary -) - -func (s ScrollRange) Union(s2 ScrollRange) ScrollRange { - return ScrollRange{ - Min: min(s.Min, s2.Min), - Max: max(s.Max, s2.Max), - } -} - -// Push the current pass mode to the pass stack and set the pass mode. -func (p PassOp) Push(o *op.Ops) PassStack { - id, mid := ops.PushOp(&o.Internal, ops.PassStack) - data := ops.Write(&o.Internal, ops.TypePassLen) - data[0] = byte(ops.TypePass) - return PassStack{ops: &o.Internal, id: id, macroID: mid} -} - -func (p PassStack) Pop() { - ops.PopOp(p.ops, ops.PassStack, p.id, p.macroID) - data := ops.Write(p.ops, ops.TypePopPassLen) - data[0] = byte(ops.TypePopPass) -} - -func (op Cursor) Add(o *op.Ops) { - data := ops.Write(&o.Internal, ops.TypeCursorLen) - data[0] = byte(ops.TypeCursor) - data[1] = byte(op) -} - -func (t Kind) String() string { - if t == Cancel { - return "Cancel" - } - var buf strings.Builder - for tt := Kind(1); tt > 0; tt <<= 1 { - if t&tt > 0 { - if buf.Len() > 0 { - buf.WriteByte('|') - } - buf.WriteString((t & tt).string()) - } - } - return buf.String() -} - -func (t Kind) string() string { - switch t { - case Press: - return "Press" - case Release: - return "Release" - case Cancel: - return "Cancel" - case Move: - return "Move" - case Drag: - return "Drag" - case Enter: - return "Enter" - case Leave: - return "Leave" - case Scroll: - return "Scroll" - default: - panic("unknown Type") - } -} - -func (p Priority) String() string { - switch p { - case Shared: - return "Shared" - case Grabbed: - return "Grabbed" - default: - panic("unknown priority") - } -} - -func (s Source) String() string { - switch s { - case Mouse: - return "Mouse" - case Touch: - return "Touch" - default: - panic("unknown source") - } -} - -// Contain reports whether the set b contains -// all of the buttons. -func (b Buttons) Contain(buttons Buttons) bool { - return b&buttons == buttons -} - -func (b Buttons) String() string { - var strs []string - if b.Contain(ButtonPrimary) { - strs = append(strs, "ButtonPrimary") - } - if b.Contain(ButtonSecondary) { - strs = append(strs, "ButtonSecondary") - } - if b.Contain(ButtonTertiary) { - strs = append(strs, "ButtonTertiary") - } - if b.Contain(ButtonQuaternary) { - strs = append(strs, "ButtonQuaternary") - } - if b.Contain(ButtonQuinary) { - strs = append(strs, "ButtonQuinary") - } - return strings.Join(strs, "|") -} - -func (c Cursor) String() string { - switch c { - case CursorDefault: - return "Default" - case CursorNone: - return "None" - case CursorText: - return "Text" - case CursorVerticalText: - return "VerticalText" - case CursorPointer: - return "Pointer" - case CursorCrosshair: - return "Crosshair" - case CursorAllScroll: - return "AllScroll" - case CursorColResize: - return "ColResize" - case CursorRowResize: - return "RowResize" - case CursorGrab: - return "Grab" - case CursorGrabbing: - return "Grabbing" - case CursorNotAllowed: - return "NotAllowed" - case CursorWait: - return "Wait" - case CursorProgress: - return "Progress" - case CursorNorthWestResize: - return "NorthWestResize" - case CursorNorthEastResize: - return "NorthEastResize" - case CursorSouthWestResize: - return "SouthWestResize" - case CursorSouthEastResize: - return "SouthEastResize" - case CursorNorthSouthResize: - return "NorthSouthResize" - case CursorEastWestResize: - return "EastWestResize" - case CursorWestResize: - return "WestResize" - case CursorEastResize: - return "EastResize" - case CursorNorthResize: - return "NorthResize" - case CursorSouthResize: - return "SouthResize" - case CursorNorthEastSouthWestResize: - return "NorthEastSouthWestResize" - case CursorNorthWestSouthEastResize: - return "NorthWestSouthEastResize" - default: - panic("unknown Type") - } -} - -func (Event) ImplementsEvent() {} - -func (GrabCmd) ImplementsCommand() {} - -func (Filter) ImplementsFilter() {} diff --git a/gio/io/pointer/pointer_test.go b/gio/io/pointer/pointer_test.go deleted file mode 100644 index f0a4380..0000000 --- a/gio/io/pointer/pointer_test.go +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package pointer - -import ( - "testing" -) - -func TestTypeString(t *testing.T) { - for _, tc := range []struct { - typ Kind - res string - }{ - {Cancel, "Cancel"}, - {Press, "Press"}, - {Release, "Release"}, - {Move, "Move"}, - {Drag, "Drag"}, - {Enter, "Enter"}, - {Leave, "Leave"}, - {Scroll, "Scroll"}, - {Enter | Leave, "Enter|Leave"}, - {Press | Release, "Press|Release"}, - {Enter | Leave | Press | Release, "Press|Release|Enter|Leave"}, - {Move | Scroll, "Move|Scroll"}, - } { - t.Run(tc.res, func(t *testing.T) { - if want, got := tc.res, tc.typ.String(); want != got { - t.Errorf("got %q; want %q", got, want) - } - }) - } -} diff --git a/gio/io/semantic/semantic.go b/gio/io/semantic/semantic.go deleted file mode 100644 index 4978389..0000000 --- a/gio/io/semantic/semantic.go +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package semantic provides operations for semantic descriptions of a user -// interface, to facilitate presentation and interaction in external software -// such as screen readers. -// -// Semantic descriptions are organized in a tree, with clip operations as -// nodes. Operations in this package are associated with the current semantic -// node, that is the most recent pushed clip operation. -package semantic - -import ( - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// LabelOp provides the content of a textual component. -type LabelOp string - -// DescriptionOp describes a component. -type DescriptionOp string - -// ClassOp provides the component class. -type ClassOp int - -const ( - Unknown ClassOp = iota - Button - CheckBox - Editor - RadioButton - Switch -) - -// SelectedOp describes the selected state for components that have -// boolean state. -type SelectedOp bool - -// EnabledOp describes the enabled state. -type EnabledOp bool - -func (l LabelOp) Add(o *op.Ops) { - data := ops.Write1String(&o.Internal, ops.TypeSemanticLabelLen, string(l)) - data[0] = byte(ops.TypeSemanticLabel) -} - -func (d DescriptionOp) Add(o *op.Ops) { - data := ops.Write1String(&o.Internal, ops.TypeSemanticDescLen, string(d)) - data[0] = byte(ops.TypeSemanticDesc) -} - -func (c ClassOp) Add(o *op.Ops) { - data := ops.Write(&o.Internal, ops.TypeSemanticClassLen) - data[0] = byte(ops.TypeSemanticClass) - data[1] = byte(c) -} - -func (s SelectedOp) Add(o *op.Ops) { - data := ops.Write(&o.Internal, ops.TypeSemanticSelectedLen) - data[0] = byte(ops.TypeSemanticSelected) - if s { - data[1] = 1 - } -} - -func (e EnabledOp) Add(o *op.Ops) { - data := ops.Write(&o.Internal, ops.TypeSemanticEnabledLen) - data[0] = byte(ops.TypeSemanticEnabled) - if e { - data[1] = 1 - } -} - -func (c ClassOp) String() string { - switch c { - case Unknown: - return "Unknown" - case Button: - return "Button" - case CheckBox: - return "CheckBox" - case Editor: - return "Editor" - case RadioButton: - return "RadioButton" - case Switch: - return "Switch" - default: - panic("invalid ClassOp") - } -} diff --git a/gio/io/system/decoration.go b/gio/io/system/decoration.go deleted file mode 100644 index 01dfc24..0000000 --- a/gio/io/system/decoration.go +++ /dev/null @@ -1,77 +0,0 @@ -package system - -import ( - "strings" - - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// ActionAreaOp makes the current clip area available for -// system gestures. -// -// Note: only ActionMove is supported. -type ActionInputOp Action - -// Action is a set of window decoration actions. -type Action uint - -const ( - // ActionMinimize minimizes a window. - ActionMinimize Action = 1 << iota - // ActionMaximize maximizes a window. - ActionMaximize - // ActionUnmaximize restores a maximized window. - ActionUnmaximize - // ActionFullscreen makes a window fullscreen. - ActionFullscreen - // ActionRaise requests that the platform bring this window to the top of all open windows. - // Some platforms do not allow this except under certain circumstances, such as when - // a window from the same application already has focus. If the platform does not - // support it, this method will do nothing. - ActionRaise - // ActionCenter centers the window on the screen. - // It is ignored in Fullscreen mode and on Wayland. - ActionCenter - // ActionClose closes a window. - // Only applicable on macOS, Windows, X11 and Wayland. - ActionClose - // ActionMove moves a window directed by the user. - ActionMove -) - -func (op ActionInputOp) Add(o *op.Ops) { - data := ops.Write(&o.Internal, ops.TypeActionInputLen) - data[0] = byte(ops.TypeActionInput) - data[1] = byte(op) -} - -func (a Action) String() string { - var buf strings.Builder - for b := Action(1); a != 0; b <<= 1 { - if a&b != 0 { - if buf.Len() > 0 { - buf.WriteByte('|') - } - buf.WriteString(b.string()) - a &^= b - } - } - return buf.String() -} - -func (a Action) string() string { - switch a { - case ActionMinimize: - return "ActionMinimize" - case ActionMaximize: - return "ActionMaximize" - case ActionUnmaximize: - return "ActionUnmaximize" - case ActionClose: - return "ActionClose" - case ActionMove: - return "ActionMove" - } - return "" -} diff --git a/gio/io/system/locale.go b/gio/io/system/locale.go deleted file mode 100644 index 388d2e5..0000000 --- a/gio/io/system/locale.go +++ /dev/null @@ -1,68 +0,0 @@ -package system - -// Locale provides language information for the current system. -type Locale struct { - // Language is the BCP-47 tag for the primary language of the system. - Language string - // Direction indicates the primary direction of text and layout - // flow for the system. - Direction TextDirection -} - -const ( - axisShift = iota - progressionShift -) - -// TextDirection defines a direction for text flow. -type TextDirection byte - -const ( - // LTR is left-to-right text. - LTR TextDirection = TextDirection(Horizontal<> axisShift) -} - -// Progression returns the way that the text flows relative to the origin. -func (d TextDirection) Progression() TextProgression { - return TextProgression((d & (1 << progressionShift)) >> progressionShift) -} - -func (d TextDirection) String() string { - switch d { - case RTL: - return "RTL" - default: - return "LTR" - } -} - -// TextAxis defines the layout axis of text. -type TextAxis byte - -const ( - // Horizontal indicates text that flows along the X axis. - Horizontal TextAxis = iota - // Vertical indicates text that flows along the Y axis. - Vertical -) - -// TextProgression indicates how text flows along an axis relative to the -// origin. For these purposes, the origin is defined as the upper-left -// corner of coordinate space. -type TextProgression byte - -const ( - // FromOrigin indicates text that flows along its axis away from the - // origin (upper left corner). - FromOrigin TextProgression = iota - // TowardOrigin indicates text that flows along its axis towards the - // origin (upper left corner). - TowardOrigin -) diff --git a/gio/io/transfer/transfer.go b/gio/io/transfer/transfer.go deleted file mode 100644 index 8abd0f9..0000000 --- a/gio/io/transfer/transfer.go +++ /dev/null @@ -1,95 +0,0 @@ -// Package transfer contains operations and events for brokering data transfers. -// -// The transfer protocol is as follows: -// -// - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag -// is initiated, and an [RequestEvent] for each initiation of a data transfer. -// Sources respond to requests with [OfferCmd]. -// - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data. -// The target must close the data event after use. -// -// When a user initiates a pointer-guided drag and drop transfer, the -// source as well as all potential targets receive an InitiateEvent. -// Potential targets are targets with at least one MIME type in common -// with the source. When a drag gesture completes, a CancelEvent is sent -// to the source and all potential targets. -// -// Note that the RequestEvent is sent to the source upon drop. -package transfer - -import ( - "io" - - "github.com/p9c/p9/pkg/gel/gio/io/event" -) - -// OfferCmd is used by data sources as a response to a RequestEvent. -type OfferCmd struct { - Tag event.Tag - // Type is the MIME type of Data. - // It must be the Type from the corresponding RequestEvent. - Type string - // Data contains the offered data. It is closed when the - // transfer is complete or cancelled. - // Data must be kept valid until closed, and it may be used from - // a goroutine separate from the one processing the frame. - Data io.ReadCloser -} - -func (OfferCmd) ImplementsCommand() {} - -// SourceFilter filters for any [RequestEvent] that match a MIME type -// as well as [InitiateEvent] and [CancelEvent]. -// Use multiple filters to offer multiple types. -type SourceFilter struct { - // Target is a tag included in a previous event.Op. - Target event.Tag - // Type is the MIME type supported by this source. - Type string -} - -// TargetFilter filters for any [DataEvent] whose type matches a MIME type -// as well as [CancelEvent]. Use multiple filters to accept multiple types. -type TargetFilter struct { - // Target is a tag included in a previous event.Op. - Target event.Tag - // Type is the MIME type accepted by this target. - Type string -} - -// RequestEvent requests data from a data source. The source must -// respond with an OfferCmd. -type RequestEvent struct { - // Type is the first matched type between the source and the target. - Type string -} - -func (RequestEvent) ImplementsEvent() {} - -// InitiateEvent is sent to a data source when a drag-and-drop -// transfer gesture is initiated. -// -// Potential data targets also receive the event. -type InitiateEvent struct{} - -func (InitiateEvent) ImplementsEvent() {} - -// CancelEvent is sent to data sources and targets to cancel the -// effects of an InitiateEvent. -type CancelEvent struct{} - -func (CancelEvent) ImplementsEvent() {} - -// DataEvent is sent to the target receiving the transfer. -type DataEvent struct { - // Type is the MIME type of Data. - Type string - // Open returns the transfer data. It is only valid to call Open in the frame - // the DataEvent is received. The caller must close the return value after use. - Open func() io.ReadCloser -} - -func (DataEvent) ImplementsEvent() {} - -func (SourceFilter) ImplementsFilter() {} -func (TargetFilter) ImplementsFilter() {} diff --git a/gio/layout/alloc_test.go b/gio/layout/alloc_test.go deleted file mode 100644 index 1692b39..0000000 --- a/gio/layout/alloc_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -//go:build !race -// +build !race - -package layout - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/op" -) - -func TestStackAllocs(t *testing.T) { - var ops op.Ops - allocs := testing.AllocsPerRun(1, func() { - ops.Reset() - gtx := Context{ - Ops: &ops, - } - Stack{}.Layout(gtx, - Stacked(func(gtx Context) Dimensions { - return Dimensions{Size: image.Point{X: 50, Y: 50}} - }), - ) - }) - if allocs != 0 { - t.Errorf("expected no allocs, got %f", allocs) - } -} - -func TestFlexAllocs(t *testing.T) { - var ops op.Ops - allocs := testing.AllocsPerRun(1, func() { - ops.Reset() - gtx := Context{ - Ops: &ops, - } - Flex{}.Layout(gtx, - Rigid(func(gtx Context) Dimensions { - return Dimensions{Size: image.Point{X: 50, Y: 50}} - }), - ) - }) - if allocs != 0 { - t.Errorf("expected no allocs, got %f", allocs) - } -} diff --git a/gio/layout/context.go b/gio/layout/context.go deleted file mode 100644 index 4b836f6..0000000 --- a/gio/layout/context.go +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package layout - -import ( - "time" - - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// Context carries the state needed by almost all layouts and widgets. -// A zero value Context never returns events, map units to pixels -// with a scale of 1.0, and returns the zero time from Now. -type Context struct { - // Constraints track the constraints for the active widget or - // layout. - Constraints Constraints - - Metric unit.Metric - // Now is the animation time. - Now time.Time - - // Locale provides information on the system's language preferences. - // BUG(whereswaldon): this field is not currently populated automatically. - // Interested users must look up and populate these values manually. - Locale system.Locale - - // Values is a map of program global data associated with the context. - // It is not for use by widgets. - Values map[string]any - - input.Source - *op.Ops -} - -// Dp converts v to pixels. -func (c Context) Dp(v unit.Dp) int { - return c.Metric.Dp(v) -} - -// Sp converts v to pixels. -func (c Context) Sp(v unit.Sp) int { - return c.Metric.Sp(v) -} - -// Disabled returns a copy of this context that don't deliver any events. -func (c Context) Disabled() Context { - c.Source = c.Source.Disabled() - return c -} diff --git a/gio/layout/doc.go b/gio/layout/doc.go deleted file mode 100644 index 45d5f39..0000000 --- a/gio/layout/doc.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package layout implements layouts common to GUI programs. - -# Constraints and dimensions - -Constraints and dimensions form the interface between layouts and -interface child elements. This package operates on Widgets, functions -that compute Dimensions from a a set of constraints for acceptable -widths and heights. Both the constraints and dimensions are maintained -in an implicit Context to keep the Widget declaration short. - -For example, to add space above a widget: - - var gtx layout.Context - - // Configure a top inset. - inset := layout.Inset{Top: 8, ...} - // Use the inset to lay out a widget. - inset.Layout(gtx, func() { - // Lay out widget and determine its size given the constraints - // in gtx.Constraints. - ... - return layout.Dimensions{...} - }) - -Note that the example does not generate any garbage even though the -Inset is transient. Layouts that don't accept user input are designed -to not escape to the heap during their use. - -Layout operations are recursive: a child in a layout operation can -itself be another layout. That way, complex user interfaces can -be created from a few generic layouts. - -This example both aligns and insets a child: - - inset := layout.Inset{...} - inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - align := layout.Alignment(...) - return align.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return widget.Layout(gtx, ...) - }) - }) - -More complex layouts such as Stack and Flex lay out multiple children, -and stateful layouts such as List accept user input. -*/ -package layout diff --git a/gio/layout/example_test.go b/gio/layout/example_test.go deleted file mode 100644 index 3f20175..0000000 --- a/gio/layout/example_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package layout_test - -import ( - "fmt" - "image" - - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -func ExampleInset() { - gtx := layout.Context{ - Ops: new(op.Ops), - // Loose constraints with no minimal size. - Constraints: layout.Constraints{ - Max: image.Point{X: 100, Y: 100}, - }, - } - - // Inset all edges by 10. - inset := layout.UniformInset(10) - dims := inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - // Lay out a 50x50 sized widget. - dims := layoutWidget(gtx, 50, 50) - fmt.Println(dims.Size) - return dims - }) - - fmt.Println(dims.Size) - - // Output: - // (50,50) - // (70,70) -} - -func ExampleDirection() { - gtx := layout.Context{ - Ops: new(op.Ops), - // Rigid constraints with both minimum and maximum set. - Constraints: layout.Exact(image.Point{X: 100, Y: 100}), - } - - dims := layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - // Lay out a 50x50 sized widget. - dims := layoutWidget(gtx, 50, 50) - fmt.Println(dims.Size) - return dims - }) - - fmt.Println(dims.Size) - - // Output: - // (50,50) - // (100,100) -} - -func ExampleFlex() { - gtx := layout.Context{ - Ops: new(op.Ops), - // Rigid constraints with both minimum and maximum set. - Constraints: layout.Exact(image.Point{X: 100, Y: 100}), - } - - layout.Flex{WeightSum: 2}.Layout(gtx, - // Rigid 10x10 widget. - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - fmt.Printf("Rigid: %v\n", gtx.Constraints) - return layoutWidget(gtx, 10, 10) - }), - // Child with 50% space allowance. - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - fmt.Printf("50%%: %v\n", gtx.Constraints) - return layoutWidget(gtx, 10, 10) - }), - ) - - // Output: - // Rigid: {(0,100) (100,100)} - // 50%: {(45,100) (45,100)} -} - -func ExampleStack() { - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{ - Max: image.Point{X: 100, Y: 100}, - }, - } - - layout.Stack{}.Layout(gtx, - // Force widget to the same size as the second. - layout.Expanded(func(gtx layout.Context) layout.Dimensions { - fmt.Printf("Expand: %v\n", gtx.Constraints) - return layoutWidget(gtx, 10, 10) - }), - // Rigid 50x50 widget. - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - return layoutWidget(gtx, 50, 50) - }), - ) - - // Output: - // Expand: {(50,50) (100,100)} -} - -func ExampleBackground() { - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{ - Max: image.Point{X: 100, Y: 100}, - }, - } - - layout.Background{}.Layout(gtx, - // Force widget to the same size as the second. - func(gtx layout.Context) layout.Dimensions { - fmt.Printf("Expand: %v\n", gtx.Constraints) - return layoutWidget(gtx, 10, 10) - }, - // Rigid 50x50 widget. - func(gtx layout.Context) layout.Dimensions { - return layoutWidget(gtx, 50, 50) - }, - ) - - // Output: - // Expand: {(50,50) (100,100)} -} - -func ExampleList() { - gtx := layout.Context{ - Ops: new(op.Ops), - // Rigid constraints with both minimum and maximum set. - Constraints: layout.Exact(image.Point{X: 100, Y: 100}), - } - - // The list is 1e6 elements, but only 5 fit the constraints. - const listLen = 1e6 - - var list layout.List - list.Layout(gtx, listLen, func(gtx layout.Context, i int) layout.Dimensions { - return layoutWidget(gtx, 20, 20) - }) - - fmt.Println(list.Position.Count) - - // Output: - // 5 -} - -func layoutWidget(ctx layout.Context, width, height int) layout.Dimensions { - return layout.Dimensions{ - Size: image.Point{ - X: width, - Y: height, - }, - } -} diff --git a/gio/layout/flex.go b/gio/layout/flex.go deleted file mode 100644 index 46c46ed..0000000 --- a/gio/layout/flex.go +++ /dev/null @@ -1,250 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package layout - -import ( - "image" - - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Flex lays out child elements along an axis, -// according to alignment and weights. -type Flex struct { - // Axis is the main axis, either Horizontal or Vertical. - Axis Axis - // Spacing controls the distribution of space left after - // layout. - Spacing Spacing - // Alignment is the alignment in the cross axis. - Alignment Alignment - // WeightSum is the sum of weights used for the weighted - // size of Flexed children. If WeightSum is zero, the sum - // of all Flexed weights is used. - WeightSum float32 -} - -// FlexChild is the descriptor for a Flex child. -type FlexChild struct { - flex bool - weight float32 - - widget Widget -} - -// Spacing determine the spacing mode for a Flex. -type Spacing uint8 - -const ( - // SpaceEnd leaves space at the end. - SpaceEnd Spacing = iota - // SpaceStart leaves space at the start. - SpaceStart - // SpaceSides shares space between the start and end. - SpaceSides - // SpaceAround distributes space evenly between children, - // with half as much space at the start and end. - SpaceAround - // SpaceBetween distributes space evenly between children, - // leaving no space at the start and end. - SpaceBetween - // SpaceEvenly distributes space evenly between children and - // at the start and end. - SpaceEvenly -) - -// Rigid returns a Flex child with a maximal constraint of the -// remaining space. -func Rigid(widget Widget) FlexChild { - return FlexChild{ - widget: widget, - } -} - -// Flexed returns a Flex child forced to take up weight fraction of the -// space left over from Rigid children. The fraction is weight -// divided by either the weight sum of all Flexed children or the Flex -// WeightSum if non zero. -func Flexed(weight float32, widget Widget) FlexChild { - return FlexChild{ - flex: true, - weight: weight, - widget: widget, - } -} - -// Layout a list of children. The position of the children are -// determined by the specified order, but Rigid children are laid out -// before Flexed children. -func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions { - size := 0 - cs := gtx.Constraints - mainMin, mainMax := f.Axis.mainConstraint(cs) - crossMin, crossMax := f.Axis.crossConstraint(cs) - remaining := mainMax - var totalWeight float32 - cgtx := gtx - // Note: previously the scratch space was inside FlexChild. - // child.call.Add(gtx.Ops) confused the go escape analysis and caused the - // entired children slice to be allocated on the heap, including all widgets - // in it. This produced a lot of object allocations. Now the scratch space - // is separate from children, and for cases len(children) <= 32, we will - // allocate the scratch space on the stack. For cases len(children) > 32, - // only the scratch space gets allocated from the heap, during append. - type scratchSpace struct { - call op.CallOp - dims Dimensions - } - var scratchArray [32]scratchSpace - scratch := scratchArray[:0] - scratch = append(scratch, make([]scratchSpace, len(children))...) - // Lay out Rigid children. - for i, child := range children { - if child.flex { - totalWeight += child.weight - continue - } - macro := op.Record(gtx.Ops) - cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax) - dims := child.widget(cgtx) - c := macro.Stop() - sz := f.Axis.Convert(dims.Size).X - size += sz - remaining -= sz - if remaining < 0 { - remaining = 0 - } - scratch[i].call = c - scratch[i].dims = dims - } - if w := f.WeightSum; w != 0 { - totalWeight = w - } - // fraction is the rounding error from a Flex weighting. - var fraction float32 - flexTotal := remaining - // Lay out Flexed children. - for i, child := range children { - if !child.flex { - continue - } - var flexSize int - if remaining > 0 && totalWeight > 0 { - // Apply weight and add any leftover fraction from a - // previous Flexed. - childSize := float32(flexTotal) * child.weight / totalWeight - flexSize = int(childSize + fraction + .5) - fraction = childSize - float32(flexSize) - if flexSize > remaining { - flexSize = remaining - } - } - macro := op.Record(gtx.Ops) - cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin, crossMax) - dims := child.widget(cgtx) - c := macro.Stop() - sz := f.Axis.Convert(dims.Size).X - size += sz - remaining -= sz - if remaining < 0 { - remaining = 0 - } - scratch[i].call = c - scratch[i].dims = dims - } - maxCross := crossMin - var maxBaseline int - for _, scratchChild := range scratch { - if c := f.Axis.Convert(scratchChild.dims.Size).Y; c > maxCross { - maxCross = c - } - if b := scratchChild.dims.Size.Y - scratchChild.dims.Baseline; b > maxBaseline { - maxBaseline = b - } - } - var space int - if mainMin > size { - space = mainMin - size - } - var mainSize int - switch f.Spacing { - case SpaceSides: - mainSize += space / 2 - case SpaceStart: - mainSize += space - case SpaceEvenly: - mainSize += space / (1 + len(children)) - case SpaceAround: - if len(children) > 0 { - mainSize += space / (len(children) * 2) - } - } - for i, scratchChild := range scratch { - dims := scratchChild.dims - b := dims.Size.Y - dims.Baseline - var cross int - switch f.Alignment { - case End: - cross = maxCross - f.Axis.Convert(dims.Size).Y - case Middle: - cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2 - case Baseline: - if f.Axis == Horizontal { - cross = maxBaseline - b - } - } - pt := f.Axis.Convert(image.Pt(mainSize, cross)) - trans := op.Offset(pt).Push(gtx.Ops) - scratchChild.call.Add(gtx.Ops) - trans.Pop() - mainSize += f.Axis.Convert(dims.Size).X - if i < len(children)-1 { - switch f.Spacing { - case SpaceEvenly: - mainSize += space / (1 + len(children)) - case SpaceAround: - if len(children) > 0 { - mainSize += space / len(children) - } - case SpaceBetween: - if len(children) > 1 { - mainSize += space / (len(children) - 1) - } - } - } - } - switch f.Spacing { - case SpaceSides: - mainSize += space / 2 - case SpaceEnd: - mainSize += space - case SpaceEvenly: - mainSize += space / (1 + len(children)) - case SpaceAround: - if len(children) > 0 { - mainSize += space / (len(children) * 2) - } - } - sz := f.Axis.Convert(image.Pt(mainSize, maxCross)) - sz = cs.Constrain(sz) - return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline} -} - -func (s Spacing) String() string { - switch s { - case SpaceEnd: - return "SpaceEnd" - case SpaceStart: - return "SpaceStart" - case SpaceSides: - return "SpaceSides" - case SpaceAround: - return "SpaceAround" - case SpaceBetween: - return "SpaceAround" - case SpaceEvenly: - return "SpaceEvenly" - default: - panic("unreachable") - } -} diff --git a/gio/layout/layout.go b/gio/layout/layout.go deleted file mode 100644 index 9092d0b..0000000 --- a/gio/layout/layout.go +++ /dev/null @@ -1,344 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package layout - -import ( - "image" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// Constraints represent the minimum and maximum size of a widget. -// -// A widget does not have to treat its constraints as "hard". For -// example, if it's passed a constraint with a minimum size that's -// smaller than its actual minimum size, it should return its minimum -// size dimensions instead. Parent widgets should deal appropriately -// with child widgets that return dimensions that do not fit their -// constraints (for example, by clipping). -type Constraints struct { - Min, Max image.Point -} - -// Dimensions are the resolved size and baseline for a widget. -// -// Baseline is the distance from the bottom of a widget to the baseline of -// any text it contains (or 0). The purpose is to be able to align text -// that span multiple widgets. -type Dimensions struct { - Size image.Point - Baseline int -} - -// Axis is the Horizontal or Vertical direction. -type Axis uint8 - -// Alignment is the mutual alignment of a list of widgets. -type Alignment uint8 - -// Direction is the alignment of widgets relative to a containing -// space. -type Direction uint8 - -// Widget is a function scope for drawing, processing events and -// computing dimensions for a user interface element. -type Widget func(gtx Context) Dimensions - -const ( - Start Alignment = iota - End - Middle - Baseline -) - -const ( - NW Direction = iota - N - NE - E - SE - S - SW - W - Center -) - -const ( - Horizontal Axis = iota - Vertical -) - -// Exact returns the Constraints with the minimum and maximum size -// set to size. -func Exact(size image.Point) Constraints { - return Constraints{ - Min: size, Max: size, - } -} - -// FPt converts an point to a f32.Point. -func FPt(p image.Point) f32.Point { - return f32.Point{ - X: float32(p.X), Y: float32(p.Y), - } -} - -// Constrain a size so each dimension is in the range [min;max]. -func (c Constraints) Constrain(size image.Point) image.Point { - if min := c.Min.X; size.X < min { - size.X = min - } - if min := c.Min.Y; size.Y < min { - size.Y = min - } - if max := c.Max.X; size.X > max { - size.X = max - } - if max := c.Max.Y; size.Y > max { - size.Y = max - } - return size -} - -// AddMin returns a copy of Constraints with the Min constraint enlarged by up to delta -// while still fitting within the Max constraint. The Max is unchanged, and the Min constraint -// will not go negative. -func (c Constraints) AddMin(delta image.Point) Constraints { - c.Min = c.Min.Add(delta) - if c.Min.X < 0 { - c.Min.X = 0 - } - if c.Min.Y < 0 { - c.Min.Y = 0 - } - c.Min = c.Constrain(c.Min) - return c -} - -// SubMax returns a copy of Constraints with the Max constraint shrunk by up to delta -// while not going negative. The values of delta are expected to be positive. -// The Min constraint is adjusted to fit within the new Max constraint. -func (c Constraints) SubMax(delta image.Point) Constraints { - c.Max = c.Max.Sub(delta) - if c.Max.X < 0 { - c.Max.X = 0 - } - if c.Max.Y < 0 { - c.Max.Y = 0 - } - c.Min = c.Constrain(c.Min) - return c -} - -// Inset adds space around a widget by decreasing its maximum -// constraints. The minimum constraints will be adjusted to ensure -// they do not exceed the maximum. -type Inset struct { - Top, Bottom, Left, Right unit.Dp -} - -// Layout a widget. -func (in Inset) Layout(gtx Context, w Widget) Dimensions { - top := gtx.Dp(in.Top) - right := gtx.Dp(in.Right) - bottom := gtx.Dp(in.Bottom) - left := gtx.Dp(in.Left) - mcs := gtx.Constraints - mcs.Max.X -= left + right - if mcs.Max.X < 0 { - left = 0 - right = 0 - mcs.Max.X = 0 - } - if mcs.Min.X > mcs.Max.X { - mcs.Min.X = mcs.Max.X - } - mcs.Max.Y -= top + bottom - if mcs.Max.Y < 0 { - bottom = 0 - top = 0 - mcs.Max.Y = 0 - } - if mcs.Min.Y > mcs.Max.Y { - mcs.Min.Y = mcs.Max.Y - } - gtx.Constraints = mcs - trans := op.Offset(image.Pt(left, top)).Push(gtx.Ops) - dims := w(gtx) - trans.Pop() - return Dimensions{ - Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}), - Baseline: dims.Baseline + bottom, - } -} - -// UniformInset returns an Inset with a single inset applied to all -// edges. -func UniformInset(v unit.Dp) Inset { - return Inset{Top: v, Right: v, Bottom: v, Left: v} -} - -// Layout a widget according to the direction. -// The widget is called with the context constraints minimum cleared. -func (d Direction) Layout(gtx Context, w Widget) Dimensions { - macro := op.Record(gtx.Ops) - csn := gtx.Constraints.Min - switch d { - case N, S: - gtx.Constraints.Min.Y = 0 - case E, W: - gtx.Constraints.Min.X = 0 - default: - gtx.Constraints.Min = image.Point{} - } - dims := w(gtx) - call := macro.Stop() - sz := dims.Size - if sz.X < csn.X { - sz.X = csn.X - } - if sz.Y < csn.Y { - sz.Y = csn.Y - } - - p := d.Position(dims.Size, sz) - defer op.Offset(p).Push(gtx.Ops).Pop() - call.Add(gtx.Ops) - - return Dimensions{ - Size: sz, - Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y, - } -} - -// Position calculates widget position according to the direction. -func (d Direction) Position(widget, bounds image.Point) image.Point { - var p image.Point - - switch d { - case N, S, Center: - p.X = (bounds.X - widget.X) / 2 - case NE, SE, E: - p.X = bounds.X - widget.X - } - - switch d { - case W, Center, E: - p.Y = (bounds.Y - widget.Y) / 2 - case SW, S, SE: - p.Y = bounds.Y - widget.Y - } - - return p -} - -// Spacer adds space between widgets. -type Spacer struct { - Width, Height unit.Dp -} - -func (s Spacer) Layout(gtx Context) Dimensions { - return Dimensions{ - Size: gtx.Constraints.Constrain(image.Point{ - X: gtx.Dp(s.Width), - Y: gtx.Dp(s.Height), - }), - } -} - -func (a Alignment) String() string { - switch a { - case Start: - return "Start" - case End: - return "End" - case Middle: - return "Middle" - case Baseline: - return "Baseline" - default: - panic("unreachable") - } -} - -// Convert a point in (x, y) coordinates to (main, cross) coordinates, -// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged -// for the horizontal axis, or (y, x) for the vertical axis. -func (a Axis) Convert(pt image.Point) image.Point { - if a == Horizontal { - return pt - } - return image.Pt(pt.Y, pt.X) -} - -// FConvert a point in (x, y) coordinates to (main, cross) coordinates, -// or vice versa. Specifically, FConvert((x, y)) returns (x, y) unchanged -// for the horizontal axis, or (y, x) for the vertical axis. -func (a Axis) FConvert(pt f32.Point) f32.Point { - if a == Horizontal { - return pt - } - return f32.Pt(pt.Y, pt.X) -} - -// mainConstraint returns the min and max main constraints for axis a. -func (a Axis) mainConstraint(cs Constraints) (int, int) { - if a == Horizontal { - return cs.Min.X, cs.Max.X - } - return cs.Min.Y, cs.Max.Y -} - -// crossConstraint returns the min and max cross constraints for axis a. -func (a Axis) crossConstraint(cs Constraints) (int, int) { - if a == Horizontal { - return cs.Min.Y, cs.Max.Y - } - return cs.Min.X, cs.Max.X -} - -// constraints returns the constraints for axis a. -func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints { - if a == Horizontal { - return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)} - } - return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)} -} - -func (a Axis) String() string { - switch a { - case Horizontal: - return "Horizontal" - case Vertical: - return "Vertical" - default: - panic("unreachable") - } -} - -func (d Direction) String() string { - switch d { - case NW: - return "NW" - case N: - return "N" - case NE: - return "NE" - case E: - return "E" - case SE: - return "SE" - case S: - return "S" - case SW: - return "SW" - case W: - return "W" - case Center: - return "Center" - default: - panic("unreachable") - } -} diff --git a/gio/layout/layout_test.go b/gio/layout/layout_test.go deleted file mode 100644 index 3b4ddfe..0000000 --- a/gio/layout/layout_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package layout - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/op" -) - -func TestStack(t *testing.T) { - gtx := Context{ - Ops: new(op.Ops), - Constraints: Constraints{ - Max: image.Pt(100, 100), - }, - } - exp := image.Point{X: 60, Y: 70} - dims := Stack{Alignment: Center}.Layout(gtx, - Expanded(func(gtx Context) Dimensions { - return Dimensions{Size: exp} - }), - Stacked(func(gtx Context) Dimensions { - return Dimensions{Size: image.Point{X: 50, Y: 50}} - }), - ) - if got := dims.Size; got != exp { - t.Errorf("Stack ignored Expanded size, got %v expected %v", got, exp) - } -} - -func TestFlex(t *testing.T) { - gtx := Context{ - Ops: new(op.Ops), - Constraints: Constraints{ - Min: image.Pt(100, 100), - Max: image.Pt(100, 100), - }, - } - dims := Flex{}.Layout(gtx) - if got := dims.Size; got != gtx.Constraints.Min { - t.Errorf("Flex ignored minimum constraints, got %v expected %v", got, gtx.Constraints.Min) - } -} - -func TestDirection(t *testing.T) { - max := image.Pt(100, 100) - for _, tc := range []struct { - dir Direction - exp image.Point - }{ - {N, image.Pt(max.X, 0)}, - {S, image.Pt(max.X, 0)}, - {E, image.Pt(0, max.Y)}, - {W, image.Pt(0, max.Y)}, - {NW, image.Pt(0, 0)}, - {NE, image.Pt(0, 0)}, - {SE, image.Pt(0, 0)}, - {SW, image.Pt(0, 0)}, - {Center, image.Pt(0, 0)}, - } { - t.Run(tc.dir.String(), func(t *testing.T) { - gtx := Context{ - Ops: new(op.Ops), - Constraints: Exact(max), - } - var min image.Point - tc.dir.Layout(gtx, func(gtx Context) Dimensions { - min = gtx.Constraints.Min - return Dimensions{} - }) - if got, exp := min, tc.exp; got != exp { - t.Errorf("got %v; expected %v", got, exp) - } - }) - } -} - -func TestConstraints(t *testing.T) { - type testcase struct { - name string - in Constraints - subMax image.Point - addMin image.Point - expected Constraints - } - for _, tc := range []testcase{ - { - name: "no-op", - in: Constraints{Max: image.Pt(100, 100)}, - expected: Constraints{Max: image.Pt(100, 100)}, - }, - { - name: "shrink max", - in: Constraints{Max: image.Pt(100, 100)}, - subMax: image.Pt(25, 25), - expected: Constraints{Max: image.Pt(75, 75)}, - }, - { - name: "shrink max below min", - in: Constraints{Max: image.Pt(100, 100), Min: image.Pt(50, 50)}, - subMax: image.Pt(75, 75), - expected: Constraints{Max: image.Pt(25, 25), Min: image.Pt(25, 25)}, - }, - { - name: "shrink max below zero", - in: Constraints{Max: image.Pt(100, 100), Min: image.Pt(50, 50)}, - subMax: image.Pt(125, 125), - expected: Constraints{Max: image.Pt(0, 0), Min: image.Pt(0, 0)}, - }, - { - name: "enlarge min", - in: Constraints{Max: image.Pt(100, 100)}, - addMin: image.Pt(25, 25), - expected: Constraints{Max: image.Pt(100, 100), Min: image.Pt(25, 25)}, - }, - { - name: "enlarge min beyond max", - in: Constraints{Max: image.Pt(100, 100)}, - addMin: image.Pt(125, 125), - expected: Constraints{Max: image.Pt(100, 100), Min: image.Pt(100, 100)}, - }, - { - name: "decrease min below zero", - in: Constraints{Max: image.Pt(100, 100), Min: image.Pt(50, 50)}, - addMin: image.Pt(-125, -125), - expected: Constraints{Max: image.Pt(100, 100), Min: image.Pt(0, 0)}, - }, - } { - t.Run(tc.name, func(t *testing.T) { - start := tc.in - if tc.subMax != (image.Point{}) { - start = start.SubMax(tc.subMax) - } - if tc.addMin != (image.Point{}) { - start = start.AddMin(tc.addMin) - } - if start != tc.expected { - t.Errorf("expected %#+v, got %#+v", tc.expected, start) - } - }) - } -} diff --git a/gio/layout/list.go b/gio/layout/list.go deleted file mode 100644 index 3399bf5..0000000 --- a/gio/layout/list.go +++ /dev/null @@ -1,396 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package layout - -import ( - "image" - "math" - - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -type scrollChild struct { - size image.Point - call op.CallOp -} - -// List displays a subsection of a potentially infinitely -// large underlying list. List accepts user input to scroll -// the subsection. -type List struct { - Axis Axis - // ScrollToEnd instructs the list to stay scrolled to the far end position - // once reached. A List with ScrollToEnd == true and Position.BeforeEnd == - // false draws its content with the last item at the bottom of the list - // area. - ScrollToEnd bool - // Alignment is the cross axis alignment of list elements. - Alignment Alignment - // ScrollAnyAxis allows any scroll axis to scroll the list, not just the main axis. - ScrollAnyAxis bool - - cs Constraints - scroll gesture.Scroll - scrollDelta int - - // Position is updated during Layout. To save the list scroll position, - // just save Position after Layout finishes. To scroll the list - // programmatically, update Position (e.g. restore it from a saved value) - // before calling Layout. - Position Position - - len int - - // maxSize is the total size of visible children. - maxSize int - children []scrollChild - dir iterationDir -} - -// ListElement is a function that computes the dimensions of -// a list element. -type ListElement func(gtx Context, index int) Dimensions - -type iterationDir uint8 - -// Position is a List scroll offset represented as an offset from the top edge -// of a child element. -type Position struct { - // BeforeEnd tracks whether the List position is before the very end. We - // use "before end" instead of "at end" so that the zero value of a - // Position struct is useful. - // - // When laying out a list, if ScrollToEnd is true and BeforeEnd is false, - // then First and Offset are ignored, and the list is drawn with the last - // item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored. - BeforeEnd bool - // First is the index of the first visible child. - First int - // Offset is the distance in pixels from the leading edge to the child at index - // First. - Offset int - // OffsetLast is the signed distance in pixels from the trailing edge to the - // bottom edge of the child at index First+Count. - OffsetLast int - // Count is the number of visible children. - Count int - // Length is the estimated total size of all children, measured in pixels. - Length int -} - -const ( - iterateNone iterationDir = iota - iterateForward - iterateBackward -) - -const inf = 1e6 - -// init prepares the list for iterating through its children with next. -func (l *List) init(gtx Context, len int) { - if l.more() { - panic("unfinished child") - } - l.cs = gtx.Constraints - l.maxSize = 0 - l.children = l.children[:0] - l.len = len - l.update(gtx) - if l.Position.First < 0 { - l.Position.Offset = 0 - l.Position.First = 0 - } - if l.scrollToEnd() || l.Position.First > len { - l.Position.Offset = 0 - l.Position.First = len - } -} - -// Layout a List of len items, where each item is implicitly defined -// by the callback w. Layout can handle very large lists because it only calls -// w to fill its viewport and the distance scrolled, if any. -func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions { - l.init(gtx, len) - crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints) - gtx.Constraints = l.Axis.constraints(0, inf, crossMin, crossMax) - macro := op.Record(gtx.Ops) - laidOutTotalLength := 0 - numLaidOut := 0 - - for l.next(); l.more(); l.next() { - child := op.Record(gtx.Ops) - dims := w(gtx, l.index()) - call := child.Stop() - l.end(dims, call) - laidOutTotalLength += l.Axis.Convert(dims.Size).X - numLaidOut++ - } - - if numLaidOut > 0 { - l.Position.Length = laidOutTotalLength * len / numLaidOut - } else { - l.Position.Length = 0 - } - return l.layout(gtx.Ops, macro) -} - -func (l *List) scrollToEnd() bool { - return l.ScrollToEnd && !l.Position.BeforeEnd -} - -// Dragging reports whether the List is being dragged. -func (l *List) Dragging() bool { - return l.scroll.State() == gesture.StateDragging -} - -func (l *List) update(gtx Context) { - min, max := int(-inf), int(inf) - if l.Position.First == 0 { - // Use the size of the invisible part as scroll boundary. - min = -l.Position.Offset - if min > 0 { - min = 0 - } - } - if l.Position.First+l.Position.Count == l.len { - max = -l.Position.OffsetLast - if max < 0 { - max = 0 - } - } - - xrange := pointer.ScrollRange{Min: min, Max: max} - yrange := pointer.ScrollRange{} - - axis := gesture.Axis(l.Axis) - if l.ScrollAnyAxis { - axis = gesture.Both - yrange = xrange - } else if l.Axis == Vertical { - xrange, yrange = yrange, xrange - } - d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, axis, xrange, yrange) - - l.scrollDelta = d - l.Position.Offset += d -} - -// next advances to the next child. -func (l *List) next() { - l.dir = l.nextDir() - // The user scroll offset is applied after scrolling to - // list end. - if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 { - l.Position.BeforeEnd = true - l.Position.Offset += l.scrollDelta - l.dir = l.nextDir() - } -} - -// index is current child's position in the underlying list. -func (l *List) index() int { - switch l.dir { - case iterateBackward: - return l.Position.First - 1 - case iterateForward: - return l.Position.First + len(l.children) - default: - panic("Index called before Next") - } -} - -// more reports whether more children are needed. -func (l *List) more() bool { - return l.dir != iterateNone -} - -func (l *List) nextDir() iterationDir { - _, vsize := l.Axis.mainConstraint(l.cs) - last := l.Position.First + len(l.children) - // Clamp offset. - if l.maxSize-l.Position.Offset < vsize && last == l.len { - l.Position.Offset = l.maxSize - vsize - } - if l.Position.Offset < 0 && l.Position.First == 0 { - l.Position.Offset = 0 - } - // Lay out an extra (invisible) child at each end to enable focus to - // move to them, triggering automatic scroll. - firstSize, lastSize := 0, 0 - if len(l.children) > 0 { - if l.Position.First > 0 { - firstChild := l.children[0] - firstSize = l.Axis.Convert(firstChild.size).X - } - if last < l.len { - lastChild := l.children[len(l.children)-1] - lastSize = l.Axis.Convert(lastChild.size).X - } - } - switch { - case len(l.children) == l.len: - return iterateNone - case l.maxSize-l.Position.Offset-lastSize < vsize: - return iterateForward - case l.Position.Offset-firstSize < 0: - return iterateBackward - } - return iterateNone -} - -// End the current child by specifying its dimensions. -func (l *List) end(dims Dimensions, call op.CallOp) { - child := scrollChild{dims.Size, call} - mainSize := l.Axis.Convert(child.size).X - l.maxSize += mainSize - switch l.dir { - case iterateForward: - l.children = append(l.children, child) - case iterateBackward: - l.children = append(l.children, scrollChild{}) - copy(l.children[1:], l.children) - l.children[0] = child - l.Position.First-- - l.Position.Offset += mainSize - default: - panic("call Next before End") - } - l.dir = iterateNone -} - -// Layout the List and return its dimensions. -func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { - if l.more() { - panic("unfinished child") - } - mainMin, mainMax := l.Axis.mainConstraint(l.cs) - children := l.children - var first scrollChild - // Skip invisible children. - for len(children) > 0 { - child := children[0] - sz := child.size - mainSize := l.Axis.Convert(sz).X - if l.Position.Offset < mainSize { - // First child is partially visible. - break - } - l.Position.First++ - l.Position.Offset -= mainSize - first = child - children = children[1:] - } - size := -l.Position.Offset - var maxCross int - var last scrollChild - for i, child := range children { - sz := l.Axis.Convert(child.size) - if c := sz.Y; c > maxCross { - maxCross = c - } - size += sz.X - if size >= mainMax { - if i < len(children)-1 { - last = children[i+1] - } - children = children[:i+1] - break - } - } - l.Position.Count = len(children) - l.Position.OffsetLast = mainMax - size - // ScrollToEnd lists are end aligned. - if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 { - l.Position.Offset -= space - } - pos := -l.Position.Offset - layout := func(child scrollChild) { - sz := l.Axis.Convert(child.size) - var cross int - switch l.Alignment { - case End: - cross = maxCross - sz.Y - case Middle: - cross = (maxCross - sz.Y) / 2 - } - childSize := sz.X - pt := l.Axis.Convert(image.Pt(pos, cross)) - trans := op.Offset(pt).Push(ops) - child.call.Add(ops) - trans.Pop() - pos += childSize - } - // Lay out leading invisible child. - if first != (scrollChild{}) { - sz := l.Axis.Convert(first.size) - pos -= sz.X - layout(first) - } - for _, child := range children { - layout(child) - } - // Lay out trailing invisible child. - if last != (scrollChild{}) { - layout(last) - } - atStart := l.Position.First == 0 && l.Position.Offset <= 0 - atEnd := l.Position.First+len(children) == l.len && mainMax >= pos - if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { - l.scroll.Stop() - } - l.Position.BeforeEnd = !atEnd - if pos < mainMin { - pos = mainMin - } - if pos > mainMax { - pos = mainMax - } - if crossMin, crossMax := l.Axis.crossConstraint(l.cs); maxCross < crossMin { - maxCross = crossMin - } else if maxCross > crossMax { - maxCross = crossMax - } - dims := l.Axis.Convert(image.Pt(pos, maxCross)) - call := macro.Stop() - defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop() - - l.scroll.Add(ops) - - call.Add(ops) - return Dimensions{Size: dims} -} - -// ScrollBy scrolls the list by a relative amount of items. -// -// Fractional scrolling may be inaccurate for items of differing -// dimensions. This includes scrolling by integer amounts if the current -// l.Position.Offset is non-zero. -func (l *List) ScrollBy(num float32) { - // Split number of items into integer and fractional parts - i, f := math.Modf(float64(num)) - - // Scroll by integer amount of items - l.Position.First += int(i) - - // Adjust Offset to account for fractional items. If Offset gets so large that it amounts to an entire item, then - // the layout code will handle that for us and adjust First and Offset accordingly. - itemHeight := float64(l.Position.Length) / float64(l.len) - l.Position.Offset += int(math.Round(itemHeight * f)) - - // First and Offset can go out of bounds, but the layout code knows how to handle that. - - // Ensure that the list pays attention to the Offset field when the scrollbar drag - // is started while the bar is at the end of the list. Without this, the scrollbar - // cannot be dragged away from the end. - l.Position.BeforeEnd = true -} - -// ScrollTo scrolls to the specified item. -func (l *List) ScrollTo(n int) { - l.Position.First = n - l.Position.Offset = 0 - l.Position.BeforeEnd = true -} diff --git a/gio/layout/list_test.go b/gio/layout/list_test.go deleted file mode 100644 index 0e28c5b..0000000 --- a/gio/layout/list_test.go +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package layout - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -func TestListPositionExtremes(t *testing.T) { - var l List - gtx := Context{ - Ops: new(op.Ops), - Constraints: Exact(image.Pt(20, 10)), - } - const n = 3 - layout := func(_ Context, idx int) Dimensions { - if idx < 0 || idx >= n { - t.Errorf("list index %d out of bounds [0;%d]", idx, n-1) - } - return Dimensions{} - } - l.Position.First = -1 - l.Layout(gtx, n, layout) - l.Position.First = n + 1 - l.Layout(gtx, n, layout) -} - -func TestEmptyList(t *testing.T) { - var l List - gtx := Context{ - Ops: new(op.Ops), - Constraints: Exact(image.Pt(20, 10)), - } - dims := l.Layout(gtx, 0, nil) - if got, want := dims.Size, gtx.Constraints.Min; got != want { - t.Errorf("got %v; want %v", got, want) - } -} - -func TestListScrollToEnd(t *testing.T) { - l := List{ - ScrollToEnd: true, - } - gtx := Context{ - Ops: new(op.Ops), - Constraints: Exact(image.Pt(20, 10)), - } - l.Layout(gtx, 1, func(gtx Context, idx int) Dimensions { - return Dimensions{ - Size: image.Pt(10, 10), - } - }) - if want, got := -10, l.Position.Offset; want != got { - t.Errorf("got offset %d, want %d", got, want) - } -} - -func TestListPosition(t *testing.T) { - _s := func(e ...event.Event) []event.Event { return e } - r := new(input.Router) - gtx := Context{ - Ops: new(op.Ops), - Constraints: Constraints{ - Max: image.Pt(20, 10), - }, - Source: r.Source(), - } - el := func(gtx Context, idx int) Dimensions { - return Dimensions{Size: image.Pt(10, 10)} - } - for _, tc := range []struct { - label string - num int - scroll []event.Event - first int - count int - offset int - last int - }{ - {label: "no item", last: 20}, - {label: "1 visible 0 hidden", num: 1, count: 1, last: 10}, - {label: "2 visible 0 hidden", num: 2, count: 2}, - {label: "2 visible 1 hidden", num: 3, count: 2}, - { - label: "3 visible 0 hidden small scroll", num: 3, count: 3, offset: 5, last: -5, - scroll: _s( - pointer.Event{ - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - Kind: pointer.Press, - Position: f32.Pt(0, 0), - }, - pointer.Event{ - Source: pointer.Mouse, - Kind: pointer.Scroll, - Scroll: f32.Pt(5, 0), - }, - pointer.Event{ - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - Kind: pointer.Release, - Position: f32.Pt(5, 0), - }, - ), - }, - { - label: "3 visible 0 hidden small scroll 2", num: 3, count: 3, offset: 3, last: -7, - scroll: _s( - pointer.Event{ - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - Kind: pointer.Press, - Position: f32.Pt(0, 0), - }, - pointer.Event{ - Source: pointer.Mouse, - Kind: pointer.Scroll, - Scroll: f32.Pt(3, 0), - }, - pointer.Event{ - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - Kind: pointer.Release, - Position: f32.Pt(5, 0), - }, - ), - }, - { - label: "2 visible 1 hidden large scroll", num: 3, count: 2, first: 1, - scroll: _s( - pointer.Event{ - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - Kind: pointer.Press, - Position: f32.Pt(0, 0), - }, - pointer.Event{ - Source: pointer.Mouse, - Kind: pointer.Scroll, - Scroll: f32.Pt(10, 0), - }, - pointer.Event{ - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - Kind: pointer.Release, - Position: f32.Pt(15, 0), - }, - ), - }, - } { - t.Run(tc.label, func(t *testing.T) { - gtx.Ops.Reset() - - var list List - // Initialize the list. - list.Layout(gtx, tc.num, el) - // Generate the scroll events. - r.Frame(gtx.Ops) - r.Queue(tc.scroll...) - // Let the list process the events. - list.Layout(gtx, tc.num, el) - - pos := list.Position - if got, want := pos.First, tc.first; got != want { - t.Errorf("List: invalid first position: got %v; want %v", got, want) - } - if got, want := pos.Count, tc.count; got != want { - t.Errorf("List: invalid number of visible children: got %v; want %v", got, want) - } - if got, want := pos.Offset, tc.offset; got != want { - t.Errorf("List: invalid first visible offset: got %v; want %v", got, want) - } - if got, want := pos.OffsetLast, tc.last; got != want { - t.Errorf("List: invalid last visible offset: got %v; want %v", got, want) - } - }) - } -} - -func TestExtraChildren(t *testing.T) { - var l List - l.Position.First = 1 - gtx := Context{ - Ops: new(op.Ops), - Constraints: Exact(image.Pt(10, 10)), - } - count := 0 - const all = 3 - l.Layout(gtx, all, func(gtx Context, idx int) Dimensions { - count++ - return Dimensions{Size: image.Pt(10, 10)} - }) - if count != all { - t.Errorf("laid out %d of %d children", count, all) - } -} diff --git a/gio/layout/stack.go b/gio/layout/stack.go deleted file mode 100644 index 5979584..0000000 --- a/gio/layout/stack.go +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package layout - -import ( - "image" - - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Stack lays out child elements on top of each other, -// according to an alignment direction. -type Stack struct { - // Alignment is the direction to align children - // smaller than the available space. - Alignment Direction -} - -// StackChild represents a child for a Stack layout. -type StackChild struct { - expanded bool - widget Widget -} - -// Stacked returns a Stack child that is laid out with no minimum -// constraints and the maximum constraints passed to Stack.Layout. -func Stacked(w Widget) StackChild { - return StackChild{ - widget: w, - } -} - -// Expanded returns a Stack child with the minimum constraints set -// to the largest Stacked child. The maximum constraints are set to -// the same as passed to Stack.Layout. -func Expanded(w Widget) StackChild { - return StackChild{ - expanded: true, - widget: w, - } -} - -// Layout a stack of children. The position of the children are -// determined by the specified order, but Stacked children are laid out -// before Expanded children. -func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions { - var maxSZ image.Point - // First lay out Stacked children. - cgtx := gtx - cgtx.Constraints.Min = image.Point{} - // Note: previously the scratch space was inside StackChild. - // child.call.Add(gtx.Ops) confused the go escape analysis and caused the - // entired children slice to be allocated on the heap, including all widgets - // in it. This produced a lot of object allocations. Now the scratch space - // is separate from children, and for cases len(children) <= 32, we will - // allocate the scratch space on the stack. For cases len(children) > 32, - // only the scratch space gets allocated from the heap, during append. - type scratchSpace struct { - call op.CallOp - dims Dimensions - } - var scratchArray [32]scratchSpace - scratch := scratchArray[:0] - scratch = append(scratch, make([]scratchSpace, len(children))...) - for i, w := range children { - if w.expanded { - continue - } - macro := op.Record(gtx.Ops) - dims := w.widget(cgtx) - call := macro.Stop() - if w := dims.Size.X; w > maxSZ.X { - maxSZ.X = w - } - if h := dims.Size.Y; h > maxSZ.Y { - maxSZ.Y = h - } - scratch[i].call = call - scratch[i].dims = dims - } - // Then lay out Expanded children. - for i, w := range children { - if !w.expanded { - continue - } - macro := op.Record(gtx.Ops) - cgtx.Constraints.Min = maxSZ - dims := w.widget(cgtx) - call := macro.Stop() - if w := dims.Size.X; w > maxSZ.X { - maxSZ.X = w - } - if h := dims.Size.Y; h > maxSZ.Y { - maxSZ.Y = h - } - scratch[i].call = call - scratch[i].dims = dims - } - - maxSZ = gtx.Constraints.Constrain(maxSZ) - var baseline int - for _, scratchChild := range scratch { - sz := scratchChild.dims.Size - var p image.Point - switch s.Alignment { - case N, S, Center: - p.X = (maxSZ.X - sz.X) / 2 - case NE, SE, E: - p.X = maxSZ.X - sz.X - } - switch s.Alignment { - case W, Center, E: - p.Y = (maxSZ.Y - sz.Y) / 2 - case SW, S, SE: - p.Y = maxSZ.Y - sz.Y - } - trans := op.Offset(p).Push(gtx.Ops) - scratchChild.call.Add(gtx.Ops) - trans.Pop() - if baseline == 0 { - if b := scratchChild.dims.Baseline; b != 0 { - baseline = b + maxSZ.Y - sz.Y - p.Y - } - } - } - return Dimensions{ - Size: maxSZ, - Baseline: baseline, - } -} - -// Background lays out single child widget on top of a background, -// centering, if necessary. -type Background struct{} - -// Layout a widget and then add a background to it. -func (Background) Layout(gtx Context, background, widget Widget) Dimensions { - macro := op.Record(gtx.Ops) - wdims := widget(gtx) - baseline := wdims.Baseline - call := macro.Stop() - - cgtx := gtx - cgtx.Constraints.Min = gtx.Constraints.Constrain(wdims.Size) - bdims := background(cgtx) - - if bdims.Size != wdims.Size { - p := image.Point{ - X: (bdims.Size.X - wdims.Size.X) / 2, - Y: (bdims.Size.Y - wdims.Size.Y) / 2, - } - baseline += (bdims.Size.Y - wdims.Size.Y) / 2 - trans := op.Offset(p).Push(gtx.Ops) - defer trans.Pop() - } - - call.Add(gtx.Ops) - - return Dimensions{ - Size: bdims.Size, - Baseline: baseline, - } -} diff --git a/gio/layout/stack_test.go b/gio/layout/stack_test.go deleted file mode 100644 index bba46b2..0000000 --- a/gio/layout/stack_test.go +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package layout - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/op" -) - -func BenchmarkStack(b *testing.B) { - gtx := Context{ - Ops: new(op.Ops), - Constraints: Constraints{ - Max: image.Point{X: 100, Y: 100}, - }, - } - b.ReportAllocs() - - for b.Loop() { - gtx.Ops.Reset() - - Stack{}.Layout(gtx, - Expanded(emptyWidget{ - Size: image.Point{X: 60, Y: 60}, - }.Layout), - Stacked(emptyWidget{ - Size: image.Point{X: 30, Y: 30}, - }.Layout), - ) - } -} - -func BenchmarkBackground(b *testing.B) { - gtx := Context{ - Ops: new(op.Ops), - Constraints: Constraints{ - Max: image.Point{X: 100, Y: 100}, - }, - } - b.ReportAllocs() - - for b.Loop() { - gtx.Ops.Reset() - - Background{}.Layout(gtx, - emptyWidget{ - Size: image.Point{X: 60, Y: 60}, - }.Layout, - emptyWidget{ - Size: image.Point{X: 30, Y: 30}, - }.Layout, - ) - } -} - -type emptyWidget struct { - Size image.Point -} - -func (w emptyWidget) Layout(gtx Context) Dimensions { - return Dimensions{Size: w.Size} -} diff --git a/gio/op/clip/clip.go b/gio/op/clip/clip.go deleted file mode 100644 index 1a2f9cc..0000000 --- a/gio/op/clip/clip.go +++ /dev/null @@ -1,354 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package clip - -import ( - "encoding/binary" - "hash/maphash" - "image" - "math" - - "github.com/p9c/p9/pkg/gel/gio/f32" - f32internal "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/internal/scene" - "github.com/p9c/p9/pkg/gel/gio/internal/stroke" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Op represents a clip area. Op intersects the current clip area with -// itself. -type Op struct { - path PathSpec - - outline bool - width float32 -} - -// Stack represents an Op pushed on the clip stack. -type Stack struct { - ops *ops.Ops - id ops.StackID - macroID uint32 -} - -var pathSeed maphash.Seed - -func init() { - pathSeed = maphash.MakeSeed() -} - -// Push saves the current clip state on the stack and updates the current -// state to the intersection of the current p. -func (p Op) Push(o *op.Ops) Stack { - id, macroID := ops.PushOp(&o.Internal, ops.ClipStack) - p.add(o) - return Stack{ops: &o.Internal, id: id, macroID: macroID} -} - -func (p Op) add(o *op.Ops) { - path := p.path - - if !path.hasSegments && p.width > 0 { - switch p.path.shape { - case ops.Rect: - b := f32internal.FRect(path.bounds) - var rect Path - rect.Begin(o) - rect.MoveTo(b.Min) - rect.LineTo(f32.Pt(b.Max.X, b.Min.Y)) - rect.LineTo(b.Max) - rect.LineTo(f32.Pt(b.Min.X, b.Max.Y)) - rect.Close() - path = rect.End() - case ops.Path: - // Nothing to do. - default: - panic("invalid empty path for shape") - } - } - bo := binary.LittleEndian - if path.hasSegments { - data := ops.Write(&o.Internal, ops.TypePathLen) - data[0] = byte(ops.TypePath) - bo.PutUint64(data[1:], path.hash) - path.spec.Add(o) - } - - bounds := path.bounds - if p.width > 0 { - // Expand bounds to cover stroke. - half := int(p.width*.5 + .5) - bounds.Min.X -= half - bounds.Min.Y -= half - bounds.Max.X += half - bounds.Max.Y += half - data := ops.Write(&o.Internal, ops.TypeStrokeLen) - data[0] = byte(ops.TypeStroke) - bo := binary.LittleEndian - bo.PutUint32(data[1:], math.Float32bits(p.width)) - } - - data := ops.Write(&o.Internal, ops.TypeClipLen) - data[0] = byte(ops.TypeClip) - bo.PutUint32(data[1:], uint32(bounds.Min.X)) - bo.PutUint32(data[5:], uint32(bounds.Min.Y)) - bo.PutUint32(data[9:], uint32(bounds.Max.X)) - bo.PutUint32(data[13:], uint32(bounds.Max.Y)) - if p.outline { - data[17] = byte(1) - } - data[18] = byte(path.shape) -} - -func (s Stack) Pop() { - ops.PopOp(s.ops, ops.ClipStack, s.id, s.macroID) - data := ops.Write(s.ops, ops.TypePopClipLen) - data[0] = byte(ops.TypePopClip) -} - -type PathSpec struct { - spec op.CallOp - // hasSegments tracks whether there are any segments in the path. - hasSegments bool - bounds image.Rectangle - shape ops.Shape - hash uint64 -} - -// Path constructs a Op clip path described by lines and -// Bézier curves, where drawing outside the Path is discarded. -// The inside-ness of a pixel is determines by the non-zero winding rule, -// similar to the SVG rule of the same name. -// -// Path generates no garbage and can be used for dynamic paths; path -// data is stored directly in the Ops list supplied to Begin. -type Path struct { - ops *ops.Ops - contour int - pen f32.Point - macro op.MacroOp - start f32.Point - hasSegments bool - bounds f32internal.Rectangle - hash maphash.Hash -} - -// Pos returns the current pen position. -func (p *Path) Pos() f32.Point { return p.pen } - -// Begin the path, storing the path data and final Op into ops. -// -// Caller must also call End to finish the drawing. -// Forgetting to call it will result in a "panic: cannot mix multi ops with single ones". -func (p *Path) Begin(o *op.Ops) { - *p = Path{ - ops: &o.Internal, - macro: op.Record(o), - contour: 1, - } - p.hash.SetSeed(pathSeed) - ops.BeginMulti(p.ops) - data := ops.WriteMulti(p.ops, ops.TypeAuxLen) - data[0] = byte(ops.TypeAux) -} - -// End returns a PathSpec ready to use in clipping operations. -func (p *Path) End() PathSpec { - p.gap() - c := p.macro.Stop() - ops.EndMulti(p.ops) - return PathSpec{ - spec: c, - hasSegments: p.hasSegments, - bounds: p.bounds.Round(), - hash: p.hash.Sum64(), - } -} - -// Move moves the pen by the amount specified by delta. -func (p *Path) Move(delta f32.Point) { - to := delta.Add(p.pen) - p.MoveTo(to) -} - -// MoveTo moves the pen to the specified absolute coordinate. -func (p *Path) MoveTo(to f32.Point) { - if p.pen == to { - return - } - p.gap() - p.end() - p.pen = to - p.start = to -} - -func (p *Path) gap() { - if p.pen != p.start { - // A closed contour starts and ends in the same point. - // This move creates a gap in the contour, register it. - data := ops.WriteMulti(p.ops, scene.CommandSize+4) - bo := binary.LittleEndian - bo.PutUint32(data[0:], uint32(p.contour)) - p.cmd(data[4:], scene.Gap(p.pen, p.start)) - } -} - -// end completes the current contour. -func (p *Path) end() { - p.contour++ -} - -// Line moves the pen by the amount specified by delta, recording a line. -func (p *Path) Line(delta f32.Point) { - to := delta.Add(p.pen) - p.LineTo(to) -} - -// LineTo moves the pen to the absolute point specified, recording a line. -func (p *Path) LineTo(to f32.Point) { - if to == p.pen { - return - } - data := ops.WriteMulti(p.ops, scene.CommandSize+4) - bo := binary.LittleEndian - bo.PutUint32(data[0:], uint32(p.contour)) - p.cmd(data[4:], scene.Line(p.pen, to)) - p.expand(p.pen) - p.expand(to) - p.pen = to -} - -func (p *Path) cmd(data []byte, c scene.Command) { - ops.EncodeCommand(data, c) - p.hash.Write(data) -} - -func (p *Path) expand(pt f32.Point) { - if !p.hasSegments { - p.hasSegments = true - p.bounds = f32internal.Rectangle{Min: pt, Max: pt} - } else { - b := p.bounds - if pt.X < b.Min.X { - b.Min.X = pt.X - } - if pt.Y < b.Min.Y { - b.Min.Y = pt.Y - } - if pt.X > b.Max.X { - b.Max.X = pt.X - } - if pt.Y > b.Max.Y { - b.Max.Y = pt.Y - } - p.bounds = b - } -} - -// Quad records a quadratic Bézier from the pen to end -// with the control point ctrl. -func (p *Path) Quad(ctrl, to f32.Point) { - ctrl = ctrl.Add(p.pen) - to = to.Add(p.pen) - p.QuadTo(ctrl, to) -} - -// QuadTo records a quadratic Bézier from the pen to end -// with the control point ctrl, with absolute coordinates. -func (p *Path) QuadTo(ctrl, to f32.Point) { - if ctrl == p.pen && to == p.pen { - return - } - data := ops.WriteMulti(p.ops, scene.CommandSize+4) - bo := binary.LittleEndian - bo.PutUint32(data[0:], uint32(p.contour)) - p.cmd(data[4:], scene.Quad(p.pen, ctrl, to)) - p.expand(p.pen) - p.expand(ctrl) - p.expand(to) - p.pen = to -} - -// ArcTo adds an elliptical arc to the path. The implied ellipse is defined -// by its focus points f1 and f2. -// The arc starts in the current point and ends angle radians along the ellipse boundary. -// The sign of angle determines the direction; positive being counter-clockwise, -// negative clockwise. -func (p *Path) ArcTo(f1, f2 f32.Point, angle float32) { - m, segments := stroke.ArcTransform(p.pen, f1, f2, angle) - for range segments { - p0 := p.pen - p1 := m.Transform(p0) - p2 := m.Transform(p1) - ctl := p1.Mul(2).Sub(p0.Add(p2).Mul(.5)) - p.QuadTo(ctl, p2) - } -} - -// Arc is like ArcTo where f1 and f2 are relative to the current position. -func (p *Path) Arc(f1, f2 f32.Point, angle float32) { - f1 = f1.Add(p.pen) - f2 = f2.Add(p.pen) - p.ArcTo(f1, f2, angle) -} - -// Cube records a cubic Bézier from the pen through -// two control points ending in to. -func (p *Path) Cube(ctrl0, ctrl1, to f32.Point) { - p.CubeTo(p.pen.Add(ctrl0), p.pen.Add(ctrl1), p.pen.Add(to)) -} - -// CubeTo records a cubic Bézier from the pen through -// two control points ending in to, with absolute coordinates. -func (p *Path) CubeTo(ctrl0, ctrl1, to f32.Point) { - if ctrl0 == p.pen && ctrl1 == p.pen && to == p.pen { - return - } - data := ops.WriteMulti(p.ops, scene.CommandSize+4) - bo := binary.LittleEndian - bo.PutUint32(data[0:], uint32(p.contour)) - p.cmd(data[4:], scene.Cubic(p.pen, ctrl0, ctrl1, to)) - p.expand(p.pen) - p.expand(ctrl0) - p.expand(ctrl1) - p.expand(to) - p.pen = to -} - -// Close closes the path contour. -func (p *Path) Close() { - if p.pen != p.start { - p.LineTo(p.start) - } - p.end() -} - -// Stroke represents a stroked path. -type Stroke struct { - Path PathSpec - // Width of the stroked path. - Width float32 -} - -// Op returns a clip operation representing the stroke. -func (s Stroke) Op() Op { - return Op{ - path: s.Path, - width: s.Width, - } -} - -// Outline represents the area inside of a path, according to the -// non-zero winding rule. -type Outline struct { - Path PathSpec -} - -// Op returns a clip operation representing the outline. -func (o Outline) Op() Op { - return Op{ - path: o.Path, - outline: true, - } -} diff --git a/gio/op/clip/clip_internal_test.go b/gio/op/clip/clip_internal_test.go deleted file mode 100644 index acf21b7..0000000 --- a/gio/op/clip/clip_internal_test.go +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package clip - -import ( - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/op" - "math" - "testing" -) - -func TestPath_MoveTo_LineTo(t *testing.T) { - var ops op.Ops - p := Path{} - p.Begin(&ops) - startPoint := f32.Pt(32, 32) - endPoint := f32.Pt(64, 64) - p.MoveTo(startPoint) - p.LineTo(endPoint) - pathSpec := p.End() - - minPoint := f32.Pt(32, 32) - maxPoint := f32.Pt(64, 64) - if pathSpec.bounds.Min == pathSpec.bounds.Max { - t.Errorf("zero path") - } - if pathSpec.bounds.Min != minPoint.Round() { - t.Errorf("pathSpec.bounds.Min = %v, want %v", pathSpec.bounds.Min, minPoint) - } - if pathSpec.bounds.Max != maxPoint.Round() { - t.Errorf("pathSpec.bounds.Max = %v, want %v", pathSpec.bounds.Max, maxPoint) - } -} - -func TestPath_MoveTo_QuadTo(t *testing.T) { - var ops op.Ops - p := Path{} - p.Begin(&ops) - startPoint := f32.Pt(32, 32) - midPoint := f32.Pt(60, 60) - p.MoveTo(startPoint) - p.QuadTo(midPoint.Sub(f32.Pt(-4, 0)), midPoint.Sub(f32.Pt(0, -4))) - pathSpec := p.End() - - minPoint := f32.Pt(32, 32) - maxPoint := f32.Pt(64, 64) - if pathSpec.bounds.Min == pathSpec.bounds.Max { - t.Errorf("zero path") - } - if pathSpec.bounds.Min != minPoint.Round() { - t.Errorf("pathSpec.bounds.Min = %v, want %v", pathSpec.bounds.Min, minPoint) - } - if pathSpec.bounds.Max != maxPoint.Round() { - t.Errorf("pathSpec.bounds.Max = %v, want %v", pathSpec.bounds.Max, maxPoint) - } -} - -func TestPath_MoveTo_ArcTo(t *testing.T) { - // We need a tolerance here because of rounding errors. - tolerance := f32.Pt(1, 1) - - var ops op.Ops - p := Path{} - p.Begin(&ops) - arcStartPoint := f32.Pt(48, 32) - arcCenterPoint := f32.Pt(48, 48) - p.MoveTo(arcStartPoint) - p.ArcTo(arcCenterPoint, arcCenterPoint, math.Pi*2) - pathSpec := p.End() - - minPoint := f32.Pt(32, 32).Sub(tolerance).Round() - maxPoint := f32.Pt(64, 64).Add(tolerance).Round() - if pathSpec.bounds.Min == pathSpec.bounds.Max { - t.Errorf("zero path") - } - if pathSpec.bounds.Min.X < minPoint.X || pathSpec.bounds.Min.Y < minPoint.Y { - t.Errorf("pathSpec.bounds.Min = %v, want %v", pathSpec.bounds.Min, minPoint) - } - if pathSpec.bounds.Max.X > maxPoint.X || pathSpec.bounds.Max.Y > maxPoint.Y { - t.Errorf("pathSpec.bounds.Max = %v, want %v", pathSpec.bounds.Max, maxPoint) - } -} - -func TestPath_MoveTo_CubeTo(t *testing.T) { - var ops op.Ops - p := Path{} - p.Begin(&ops) - startPoint := f32.Pt(32, 32) - midPoint := f32.Pt(48, 48) - endPoint := f32.Pt(64, 64) - p.MoveTo(startPoint) - p.CubeTo(midPoint.Sub(f32.Pt(-4, 0)), midPoint.Sub(f32.Pt(0, -4)), endPoint) - pathSpec := p.End() - - minPoint := f32.Pt(32, 32) - maxPoint := f32.Pt(64, 64) - if pathSpec.bounds.Min == pathSpec.bounds.Max { - t.Errorf("zero path") - } - if pathSpec.bounds.Min != minPoint.Round() { - t.Errorf("pathSpec.bounds.Min = %v, want %v", pathSpec.bounds.Min, minPoint) - } - if pathSpec.bounds.Max != maxPoint.Round() { - t.Errorf("pathSpec.bounds.Max = %v, want %v", pathSpec.bounds.Max, maxPoint) - } -} diff --git a/gio/op/clip/clip_test.go b/gio/op/clip/clip_test.go deleted file mode 100644 index 2a71682..0000000 --- a/gio/op/clip/clip_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package clip_test - -import ( - "image/color" - "math" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/gpu/headless" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -func TestPathOutline(t *testing.T) { - t.Run("closed path", func(t *testing.T) { - defer func() { - if err := recover(); err != nil { - t.Error("Outline of a closed path did panic") - } - }() - var p clip.Path - p.Begin(new(op.Ops)) - p.MoveTo(f32.Pt(300, 200)) - p.LineTo(f32.Pt(150, 200)) - p.MoveTo(f32.Pt(150, 200)) - p.ArcTo(f32.Pt(300, 200), f32.Pt(300, 200), 3*math.Pi/4) - p.LineTo(f32.Pt(300, 200)) - p.Close() - clip.Outline{Path: p.End()}.Op() - }) -} - -func TestPathBegin(t *testing.T) { - ops := new(op.Ops) - var p clip.Path - p.Begin(ops) - p.LineTo(f32.Pt(10, 10)) - p.Close() - stack := clip.Outline{Path: p.End()}.Op().Push(ops) - paint.Fill(ops, color.NRGBA{A: 255}) - stack.Pop() - w := newWindow(t, 100, 100) - if w == nil { - return - } - // The following should not panic. - _ = w.Frame(ops) -} - -func TestTransformChecks(t *testing.T) { - defer func() { - if err := recover(); err == nil { - t.Error("cross-macro Pop didn't panic") - } - }() - var ops op.Ops - st := clip.Op{}.Push(&ops) - op.Record(&ops) - st.Pop() -} - -func TestEmptyPath(t *testing.T) { - var ops op.Ops - p := clip.Path{} - p.Begin(&ops) - defer clip.Stroke{ - Path: p.End(), - Width: 3, - }.Op().Push(&ops).Pop() -} - -func newWindow(t testing.TB, width, height int) *headless.Window { - w, err := headless.NewWindow(width, height) - if err != nil { - t.Skipf("failed to create headless window, skipping: %v", err) - } - return w -} diff --git a/gio/op/clip/doc.go b/gio/op/clip/doc.go deleted file mode 100644 index 894cffd..0000000 --- a/gio/op/clip/doc.go +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package clip provides operations for defining areas that applies to operations -such as paints and pointer handlers. - -The current clip is initially the infinite set. Pushing an Op sets the clip -to the intersection of the current clip and pushed clip area. Popping the -area restores the clip to its state before pushing. - -General clipping areas are constructed with Path. Common cases such as -rectangular clip areas also exist as convenient constructors. -*/ -package clip diff --git a/gio/op/clip/shapes.go b/gio/op/clip/shapes.go deleted file mode 100644 index b389002..0000000 --- a/gio/op/clip/shapes.go +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package clip - -import ( - "image" - "math" - - "github.com/p9c/p9/pkg/gel/gio/f32" - f32internal "github.com/p9c/p9/pkg/gel/gio/internal/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Rect represents the clip area of a pixel-aligned rectangle. -type Rect image.Rectangle - -// Op returns the op for the rectangle. -func (r Rect) Op() Op { - return Op{ - outline: true, - path: r.Path(), - } -} - -// Push the clip operation on the clip stack. -func (r Rect) Push(ops *op.Ops) Stack { - return r.Op().Push(ops) -} - -// Path returns the PathSpec for the rectangle. -func (r Rect) Path() PathSpec { - return PathSpec{ - shape: ops.Rect, - bounds: image.Rectangle(r), - } -} - -// UniformRRect returns an RRect with all corner radii set to the -// provided radius. -func UniformRRect(rect image.Rectangle, radius int) RRect { - return RRect{ - Rect: rect, - SE: radius, - SW: radius, - NE: radius, - NW: radius, - } -} - -// RRect represents the clip area of a rectangle with rounded -// corners. -// -// Specify a square with corner radii equal to half the square size to -// construct a circular clip area. -type RRect struct { - Rect image.Rectangle - // The corner radii. - SE, SW, NW, NE int -} - -// Op returns the op for the rounded rectangle. -func (rr RRect) Op(ops *op.Ops) Op { - if rr.SE == 0 && rr.SW == 0 && rr.NW == 0 && rr.NE == 0 { - return Rect(rr.Rect).Op() - } - return Outline{Path: rr.Path(ops)}.Op() -} - -// Push the rectangle clip on the clip stack. -func (rr RRect) Push(ops *op.Ops) Stack { - return rr.Op(ops).Push(ops) -} - -// Path returns the PathSpec for the rounded rectangle. -func (rr RRect) Path(ops *op.Ops) PathSpec { - var p Path - p.Begin(ops) - - // https://pomax.github.io/bezierinfo/#circles_cubic. - const q = 4 * (math.Sqrt2 - 1) / 3 - const iq = 1 - q - - se, sw, nw, ne := float32(rr.SE), float32(rr.SW), float32(rr.NW), float32(rr.NE) - rrf := f32internal.FRect(rr.Rect) - w, n, e, s := rrf.Min.X, rrf.Min.Y, rrf.Max.X, rrf.Max.Y - - p.MoveTo(f32.Point{X: w + nw, Y: n}) - p.LineTo(f32.Point{X: e - ne, Y: n}) // N - p.CubeTo( // NE - f32.Point{X: e - ne*iq, Y: n}, - f32.Point{X: e, Y: n + ne*iq}, - f32.Point{X: e, Y: n + ne}) - p.LineTo(f32.Point{X: e, Y: s - se}) // E - p.CubeTo( // SE - f32.Point{X: e, Y: s - se*iq}, - f32.Point{X: e - se*iq, Y: s}, - f32.Point{X: e - se, Y: s}) - p.LineTo(f32.Point{X: w + sw, Y: s}) // S - p.CubeTo( // SW - f32.Point{X: w + sw*iq, Y: s}, - f32.Point{X: w, Y: s - sw*iq}, - f32.Point{X: w, Y: s - sw}) - p.LineTo(f32.Point{X: w, Y: n + nw}) // W - p.CubeTo( // NW - f32.Point{X: w, Y: n + nw*iq}, - f32.Point{X: w + nw*iq, Y: n}, - f32.Point{X: w + nw, Y: n}) - - return p.End() -} - -// Ellipse represents the largest axis-aligned ellipse that -// is contained in its bounds. -type Ellipse image.Rectangle - -// Op returns the op for the filled ellipse. -func (e Ellipse) Op(ops *op.Ops) Op { - return Outline{Path: e.Path(ops)}.Op() -} - -// Push the filled ellipse clip op on the clip stack. -func (e Ellipse) Push(ops *op.Ops) Stack { - return e.Op(ops).Push(ops) -} - -// Path constructs a path for the ellipse. -func (e Ellipse) Path(o *op.Ops) PathSpec { - bounds := image.Rectangle(e) - if bounds.Dx() == 0 || bounds.Dy() == 0 { - return PathSpec{shape: ops.Rect} - } - - var p Path - p.Begin(o) - - bf := f32internal.FRect(bounds) - center := bf.Max.Add(bf.Min).Mul(.5) - diam := bf.Dx() - r := diam * .5 - // We'll model the ellipse as a circle scaled in the Y - // direction. - scale := bf.Dy() / diam - - // https://pomax.github.io/bezierinfo/#circles_cubic. - const q = 4 * (math.Sqrt2 - 1) / 3 - - curve := r * q - top := f32.Point{X: center.X, Y: center.Y - r*scale} - - p.MoveTo(top) - p.CubeTo( - f32.Point{X: center.X + curve, Y: center.Y - r*scale}, - f32.Point{X: center.X + r, Y: center.Y - curve*scale}, - f32.Point{X: center.X + r, Y: center.Y}, - ) - p.CubeTo( - f32.Point{X: center.X + r, Y: center.Y + curve*scale}, - f32.Point{X: center.X + curve, Y: center.Y + r*scale}, - f32.Point{X: center.X, Y: center.Y + r*scale}, - ) - p.CubeTo( - f32.Point{X: center.X - curve, Y: center.Y + r*scale}, - f32.Point{X: center.X - r, Y: center.Y + curve*scale}, - f32.Point{X: center.X - r, Y: center.Y}, - ) - p.CubeTo( - f32.Point{X: center.X - r, Y: center.Y - curve*scale}, - f32.Point{X: center.X - curve, Y: center.Y - r*scale}, - top, - ) - ellipse := p.End() - ellipse.shape = ops.Ellipse - return ellipse -} diff --git a/gio/op/clip/shapes_test.go b/gio/op/clip/shapes_test.go deleted file mode 100644 index 3654635..0000000 --- a/gio/op/clip/shapes_test.go +++ /dev/null @@ -1,18 +0,0 @@ -package clip_test - -import ( - "image" - "image/color" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -func TestZeroEllipse(t *testing.T) { - p := image.Pt(1.0, 2.0) - e := clip.Ellipse{Min: p, Max: p} - ops := new(op.Ops) - paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, e.Op(ops)) -} diff --git a/gio/op/op.go b/gio/op/op.go deleted file mode 100644 index f3e5197..0000000 --- a/gio/op/op.go +++ /dev/null @@ -1,230 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package op implements operations for updating a user interface. - -Gio programs use operations, or ops, for describing their user -interfaces. There are operations for drawing, defining input -handlers, changing window properties as well as operations for -controlling the execution of other operations. - -Ops represents a list of operations. The most important use -for an Ops list is to describe a complete user interface update -to a ui/app.Window's Update method. - -Drawing a colored square: - - import "github.com/p9c/p9/pkg/gel/gio/unit" - import "github.com/p9c/p9/pkg/gel/gio/app" - import "github.com/p9c/p9/pkg/gel/gio/op/paint" - - var w app.Window - var e system.FrameEvent - ops := new(op.Ops) - ... - ops.Reset() - paint.ColorOp{Color: ...}.Add(ops) - paint.PaintOp{Rect: ...}.Add(ops) - e.Frame(ops) - -# State - -An Ops list can be viewed as a very simple virtual machine: it has state such -as transformation and color and execution flow can be controlled with macros. - -Some state, such as the current color, is modified directly by operations with -Add methods. Other state, such as transformation and clip shape, are -represented by stacks. - -This example sets the simple color state and pushes an offset to the -transformation stack. - - ops := new(op.Ops) - // Set the color. - paint.ColorOp{...}.Add(ops) - // Apply an offset to subsequent operations. - stack := op.Offset(...).Push(ops) - ... - // Undo the offset transformation. - stack.Pop() - -The MacroOp records a list of operations to be executed later: - - ops := new(op.Ops) - macro := op.Record(ops) - // Record operations by adding them. - ... - // End recording. - call := macro.Stop() - - // replay the recorded operations: - call.Add(ops) -*/ -package op - -import ( - "encoding/binary" - "image" - "math" - "time" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" -) - -// Ops holds a list of operations. Operations are stored in -// serialized form to avoid garbage during construction of -// the ops list. -type Ops struct { - // Internal is for internal use, despite being exported. - Internal ops.Ops -} - -// MacroOp records a list of operations for later use. -type MacroOp struct { - ops *ops.Ops - id ops.StackID - pc ops.PC -} - -// CallOp invokes the operations recorded by Record. -type CallOp struct { - // Ops is the list of operations to invoke. - ops *ops.Ops - start ops.PC - end ops.PC -} - -// InvalidateCmd requests a redraw at the given time. Use -// the zero value to request an immediate redraw. -type InvalidateCmd struct { - At time.Time -} - -// TransformOp represents a transformation that can be pushed on the -// transformation stack. -type TransformOp struct { - t f32.Affine2D -} - -// TransformStack represents a TransformOp pushed on the transformation stack. -type TransformStack struct { - id ops.StackID - macroID uint32 - ops *ops.Ops -} - -// Defer executes c after all other operations have completed, including -// previously deferred operations. -// Defer saves the transformation stack and pushes it prior to executing -// c. All other operation state is reset. -// -// Note that deferred operations are executed in first-in-first-out order, -// unlike the Go facility of the same name. -func Defer(o *Ops, c CallOp) { - if c.ops == nil { - return - } - state := ops.Save(&o.Internal) - // Wrap c in a macro that loads the saved state before execution. - m := Record(o) - state.Load() - c.Add(o) - c = m.Stop() - // A Defer is recorded as a TypeDefer followed by the - // wrapped macro. - data := ops.Write(&o.Internal, ops.TypeDeferLen) - data[0] = byte(ops.TypeDefer) - c.Add(o) -} - -// Reset the Ops, preparing it for re-use. Reset invalidates -// any recorded macros. -func (o *Ops) Reset() { - ops.Reset(&o.Internal) -} - -// Record a macro of operations. -func Record(o *Ops) MacroOp { - m := MacroOp{ - ops: &o.Internal, - id: ops.PushMacro(&o.Internal), - pc: ops.PCFor(&o.Internal), - } - // Reserve room for a macro definition. Updated in Stop. - data := ops.Write(m.ops, ops.TypeMacroLen) - data[0] = byte(ops.TypeMacro) - return m -} - -// Stop ends a previously started recording and returns an -// operation for replaying it. -func (m MacroOp) Stop() CallOp { - ops.PopMacro(m.ops, m.id) - ops.FillMacro(m.ops, m.pc) - return CallOp{ - ops: m.ops, - // Skip macro header. - start: m.pc.Add(ops.TypeMacro), - end: ops.PCFor(m.ops), - } -} - -// Add the recorded list of operations. Add -// panics if the Ops containing the recording -// has been reset. -func (c CallOp) Add(o *Ops) { - if c.ops == nil { - return - } - ops.AddCall(&o.Internal, c.ops, c.start, c.end) -} - -// Offset converts an offset to a TransformOp. -func Offset(off image.Point) TransformOp { - offf := f32.Pt(float32(off.X), float32(off.Y)) - return Affine(f32.AffineId().Offset(offf)) -} - -// Affine creates a TransformOp representing the transformation a. -func Affine(a f32.Affine2D) TransformOp { - return TransformOp{t: a} -} - -// Push the current transformation to the stack and then multiply the -// current transformation with t. -func (t TransformOp) Push(o *Ops) TransformStack { - id, macroID := ops.PushOp(&o.Internal, ops.TransStack) - t.add(o, true) - return TransformStack{ops: &o.Internal, id: id, macroID: macroID} -} - -// Add is like Push except it doesn't push the current transformation to the -// stack. -func (t TransformOp) Add(o *Ops) { - t.add(o, false) -} - -func (t TransformOp) add(o *Ops, push bool) { - data := ops.Write(&o.Internal, ops.TypeTransformLen) - data[0] = byte(ops.TypeTransform) - if push { - data[1] = 1 - } - bo := binary.LittleEndian - a, b, c, d, e, f := t.t.Elems() - bo.PutUint32(data[2:], math.Float32bits(a)) - bo.PutUint32(data[2+4*1:], math.Float32bits(b)) - bo.PutUint32(data[2+4*2:], math.Float32bits(c)) - bo.PutUint32(data[2+4*3:], math.Float32bits(d)) - bo.PutUint32(data[2+4*4:], math.Float32bits(e)) - bo.PutUint32(data[2+4*5:], math.Float32bits(f)) -} - -func (t TransformStack) Pop() { - ops.PopOp(t.ops, ops.TransStack, t.id, t.macroID) - data := ops.Write(t.ops, ops.TypePopTransformLen) - data[0] = byte(ops.TypePopTransform) -} - -func (InvalidateCmd) ImplementsCommand() {} diff --git a/gio/op/op_test.go b/gio/op/op_test.go deleted file mode 100644 index a1cd725..0000000 --- a/gio/op/op_test.go +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package op - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/internal/ops" -) - -func TestTransformChecks(t *testing.T) { - defer func() { - if err := recover(); err == nil { - t.Error("cross-macro Pop didn't panic") - } - }() - var ops Ops - trans := Offset(image.Point{}).Push(&ops) - Record(&ops) - trans.Pop() -} - -func TestIncompleteMacroReader(t *testing.T) { - var o Ops - // Record, but don't Stop it. - Record(&o) - Offset(image.Point{}).Push(&o) - - var r ops.Reader - - r.Reset(&o.Internal) - if _, more := r.Decode(); more { - t.Error("decoded an operation from a semantically empty Ops") - } -} diff --git a/gio/op/paint/doc.go b/gio/op/paint/doc.go deleted file mode 100644 index bbec006..0000000 --- a/gio/op/paint/doc.go +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package paint provides drawing operations for 2D graphics. - -The PaintOp operation fills the current clip with the current brush, taking the -current transformation into account. Drawing outside the current clip area is -ignored. - -The current brush is set by either a ColorOp for a constant color, or -ImageOp for an image, or LinearGradientOp for gradients. - -All color.NRGBA values are in the sRGB color space. -*/ -package paint diff --git a/gio/op/paint/paint.go b/gio/op/paint/paint.go deleted file mode 100644 index 77ee07a..0000000 --- a/gio/op/paint/paint.go +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package paint - -import ( - "encoding/binary" - "image" - "image/color" - "image/draw" - "math" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/internal/ops" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -// ImageFilter is the scaling filter for images. -type ImageFilter byte - -const ( - // FilterLinear uses linear interpolation for scaling. - FilterLinear ImageFilter = iota - // FilterNearest uses nearest neighbor interpolation for scaling. - FilterNearest -) - -// ImageOp sets the brush to an image. -type ImageOp struct { - Filter ImageFilter - - uniform bool - color color.NRGBA - src *image.RGBA - - // handle is a key to uniquely identify this ImageOp - // in a map of cached textures. - handle any -} - -// ColorOp sets the brush to a constant color. -type ColorOp struct { - Color color.NRGBA -} - -// LinearGradientOp sets the brush to a gradient starting at stop1 with color1 and -// ending at stop2 with color2. -type LinearGradientOp struct { - Stop1 f32.Point - Color1 color.NRGBA - Stop2 f32.Point - Color2 color.NRGBA -} - -// PaintOp fills the current clip area with the current brush. -type PaintOp struct{} - -// OpacityStack represents an opacity applied to all painting operations -// until Pop is called. -type OpacityStack struct { - id ops.StackID - macroID uint32 - ops *ops.Ops -} - -// NewImageOp creates an ImageOp backed by src. -// -// NewImageOp assumes the backing image is immutable, and may cache a -// copy of its contents in a GPU-friendly way. Create new ImageOps to -// ensure that changes to an image is reflected in the display of -// it. -func NewImageOp(src image.Image) ImageOp { - switch src := src.(type) { - case *image.Uniform: - col := color.NRGBAModel.Convert(src.C).(color.NRGBA) - return ImageOp{ - uniform: true, - color: col, - } - case *image.RGBA: - return ImageOp{ - src: src, - handle: new(int), - } - } - - sz := src.Bounds().Size() - // Copy the image into a GPU friendly format. - dst := image.NewRGBA(image.Rectangle{ - Max: sz, - }) - draw.Draw(dst, dst.Bounds(), src, src.Bounds().Min, draw.Src) - return ImageOp{ - src: dst, - handle: new(int), - } -} - -func (i ImageOp) Size() image.Point { - if i.src == nil { - return image.Point{} - } - return i.src.Bounds().Size() -} - -func (i ImageOp) Add(o *op.Ops) { - if i.uniform { - ColorOp{ - Color: i.color, - }.Add(o) - return - } else if i.src == nil || i.src.Bounds().Empty() { - return - } - data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle) - data[0] = byte(ops.TypeImage) - data[1] = byte(i.Filter) -} - -func (c ColorOp) Add(o *op.Ops) { - data := ops.Write(&o.Internal, ops.TypeColorLen) - data[0] = byte(ops.TypeColor) - data[1] = c.Color.R - data[2] = c.Color.G - data[3] = c.Color.B - data[4] = c.Color.A -} - -func (c LinearGradientOp) Add(o *op.Ops) { - data := ops.Write(&o.Internal, ops.TypeLinearGradientLen) - data[0] = byte(ops.TypeLinearGradient) - - bo := binary.LittleEndian - bo.PutUint32(data[1:], math.Float32bits(c.Stop1.X)) - bo.PutUint32(data[5:], math.Float32bits(c.Stop1.Y)) - bo.PutUint32(data[9:], math.Float32bits(c.Stop2.X)) - bo.PutUint32(data[13:], math.Float32bits(c.Stop2.Y)) - - data[17+0] = c.Color1.R - data[17+1] = c.Color1.G - data[17+2] = c.Color1.B - data[17+3] = c.Color1.A - data[21+0] = c.Color2.R - data[21+1] = c.Color2.G - data[21+2] = c.Color2.B - data[21+3] = c.Color2.A -} - -func (d PaintOp) Add(o *op.Ops) { - data := ops.Write(&o.Internal, ops.TypePaintLen) - data[0] = byte(ops.TypePaint) -} - -// FillShape fills the clip shape with a color. -func FillShape(ops *op.Ops, c color.NRGBA, shape clip.Op) { - defer shape.Push(ops).Pop() - Fill(ops, c) -} - -// Fill paints an infinitely large plane with the provided color. It -// is intended to be used with a clip.Op already in place to limit -// the painted area. Use FillShape unless you need to paint several -// times within the same clip.Op. -func Fill(ops *op.Ops, c color.NRGBA) { - ColorOp{Color: c}.Add(ops) - PaintOp{}.Add(ops) -} - -// PushOpacity creates a drawing layer with an opacity in the range [0;1]. -// The layer includes every subsequent drawing operation until [OpacityStack.Pop] -// is called. -// -// The layer is drawn in two steps. First, the layer operations are -// drawn to a separate image. Then, the image is blended on top of -// the frame, with the opacity used as the blending factor. -func PushOpacity(o *op.Ops, opacity float32) OpacityStack { - if opacity > 1 { - opacity = 1 - } - if opacity < 0 { - opacity = 0 - } - id, macroID := ops.PushOp(&o.Internal, ops.OpacityStack) - data := ops.Write(&o.Internal, ops.TypePushOpacityLen) - bo := binary.LittleEndian - data[0] = byte(ops.TypePushOpacity) - bo.PutUint32(data[1:], math.Float32bits(opacity)) - return OpacityStack{ops: &o.Internal, id: id, macroID: macroID} -} - -func (t OpacityStack) Pop() { - ops.PopOp(t.ops, ops.OpacityStack, t.id, t.macroID) - data := ops.Write(t.ops, ops.TypePopOpacityLen) - data[0] = byte(ops.TypePopOpacity) -} diff --git a/gio/shader/LICENSE b/gio/shader/LICENSE deleted file mode 100644 index 81f4733..0000000 --- a/gio/shader/LICENSE +++ /dev/null @@ -1,63 +0,0 @@ -This project is provided under the terms of the UNLICENSE or -the MIT license denoted by the following SPDX identifier: - -SPDX-License-Identifier: Unlicense OR MIT - -You may use the project under the terms of either license. - -Both licenses are reproduced below. - ----- -The MIT License (MIT) - -Copyright (c) 2019 The Gio authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. ---- - - - ---- -The UNLICENSE - -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to ---- diff --git a/gio/shader/README.md b/gio/shader/README.md deleted file mode 100644 index a2cc6bf..0000000 --- a/gio/shader/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# GPU programs for the Gio project - -This repository contains source code for the [Gio](https://gioui.org) -project. It also contains the generators and derived versions for use with the -GPU APIs supported by Gio. - -# Generating CPU fallbacks - -The `piet/gencpu.sh` script updates the piet-gpu binaries: - -``` -$ cd piet -$ ./gencpu.sh -``` - -# Development shell with Nix - -Use Nix flakes to set up a development shell with the tool available -for running `go generate`: - -``` -$ nix shell -``` - -## Issues and contributions - -See the [Gio contribution guide](https://gioui.org/doc/contribute). diff --git a/gio/shader/flake.lock b/gio/shader/flake.lock deleted file mode 100644 index a3b7617..0000000 --- a/gio/shader/flake.lock +++ /dev/null @@ -1,61 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1753489912, - "narHash": "sha256-uDCFHeXdRIgJpYmtcUxGEsZ+hYlLPBhR83fdU+vbC1s=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "13e8d35b7d6028b7198f8186bc0347c6abaa2701", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-25.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/gio/shader/flake.nix b/gio/shader/flake.nix deleted file mode 100644 index 33f3025..0000000 --- a/gio/shader/flake.nix +++ /dev/null @@ -1,21 +0,0 @@ -{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; - flake-utils.url = "github:numtide/flake-utils"; - }; - outputs = { self, nixpkgs, flake-utils }: - flake-utils.lib.eachDefaultSystem - (system: - let - pkgs = import nixpkgs { - inherit system; - }; - in - with pkgs; - { - devShells.default = mkShell { - buildInputs = [ glslang spirv-cross]; - }; - } - ); -} diff --git a/gio/shader/gio/common.h b/gio/shader/gio/common.h deleted file mode 100644 index 9b6dc59..0000000 --- a/gio/shader/gio/common.h +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -struct m3x2 { - vec3 r0; - vec3 r1; -}; - -// fboTransform is the transformation that cancels the implied transformation -// between the clip space and the framebuffer. Only two rows are returned. The -// last is implied to be [0, 0, 1]. -const m3x2 fboTransform = m3x2( -#if defined(LANG_HLSL) || defined(LANG_MSL) || defined(LANG_MSLIOS) - vec3(1.0, 0.0, 0.0), - vec3(0.0, -1.0, 0.0) -#else - vec3(1.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0) -#endif -); - -// windowTransform is the transformation that cancels the implied transformation -// between framebuffer space and window system coordinates. -const m3x2 windowTransform = m3x2( -#if defined(LANG_VULKAN) - vec3(1.0, 0.0, 0.0), - vec3(0.0, 1.0, 0.0) -#else - vec3(1.0, 0.0, 0.0), - vec3(0.0, -1.0, 0.0) -#endif -); - -vec3 transform3x2(m3x2 t, vec3 v) { - return vec3(dot(t.r0, v), dot(t.r1, v), dot(vec3(0.0, 0.0, 1.0), v)); -} diff --git a/gio/shader/gio/gen.go b/gio/shader/gio/gen.go deleted file mode 100644 index f26056a..0000000 --- a/gio/shader/gio/gen.go +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package gio - -//go:generate go run ../cmd/convertshaders -package gio -dir . diff --git a/gio/shader/gio/shaders.go b/gio/shader/gio/shaders.go deleted file mode 100644 index a718814..0000000 --- a/gio/shader/gio/shaders.go +++ /dev/null @@ -1,796 +0,0 @@ -// Code generated by build.go. DO NOT EDIT. - -package gio - -import ( - _ "embed" - "runtime" - - "github.com/p9c/p9/pkg/gel/gio/shader" -) - -var ( - Shader_blit_frag = [...]shader.Sources{ - { - Name: "blit.frag", - Inputs: []shader.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "opacity", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 1}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 112}}, - Size: 16, - }, - }, - { - Name: "blit.frag", - Inputs: []shader.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "opacity", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 1}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 96}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 112}}, - Size: 32, - }, - }, - { - Name: "blit.frag", - Inputs: []shader.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "opacity", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 1}}, - Textures: []shader.TextureBinding{{Name: "tex", Binding: 0}}, - }, - } - //go:embed zblit.frag.0.spirv - zblit_frag_0_spirv string - //go:embed zblit.frag.0.glsl100es - zblit_frag_0_glsl100es string - //go:embed zblit.frag.0.glsl150 - zblit_frag_0_glsl150 string - //go:embed zblit.frag.0.dxbc - zblit_frag_0_dxbc string - //go:embed zblit.frag.0.metallibmacos - zblit_frag_0_metallibmacos string - //go:embed zblit.frag.0.metallibios - zblit_frag_0_metallibios string - //go:embed zblit.frag.0.metallibiossimulator - zblit_frag_0_metallibiossimulator string - //go:embed zblit.frag.1.spirv - zblit_frag_1_spirv string - //go:embed zblit.frag.1.glsl100es - zblit_frag_1_glsl100es string - //go:embed zblit.frag.1.glsl150 - zblit_frag_1_glsl150 string - //go:embed zblit.frag.1.dxbc - zblit_frag_1_dxbc string - //go:embed zblit.frag.1.metallibmacos - zblit_frag_1_metallibmacos string - //go:embed zblit.frag.1.metallibios - zblit_frag_1_metallibios string - //go:embed zblit.frag.1.metallibiossimulator - zblit_frag_1_metallibiossimulator string - //go:embed zblit.frag.2.spirv - zblit_frag_2_spirv string - //go:embed zblit.frag.2.glsl100es - zblit_frag_2_glsl100es string - //go:embed zblit.frag.2.glsl150 - zblit_frag_2_glsl150 string - //go:embed zblit.frag.2.dxbc - zblit_frag_2_dxbc string - //go:embed zblit.frag.2.metallibmacos - zblit_frag_2_metallibmacos string - //go:embed zblit.frag.2.metallibios - zblit_frag_2_metallibios string - //go:embed zblit.frag.2.metallibiossimulator - zblit_frag_2_metallibiossimulator string - Shader_blit_vert = shader.Sources{ - Name: "blit.vert", - Inputs: []shader.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.opacity", Type: 0x0, Size: 1, Offset: 48}, {Name: "_block.fbo", Type: 0x0, Size: 1, Offset: 52}}, - Size: 56, - }, - } - //go:embed zblit.vert.0.spirv - zblit_vert_0_spirv string - //go:embed zblit.vert.0.glsl100es - zblit_vert_0_glsl100es string - //go:embed zblit.vert.0.glsl150 - zblit_vert_0_glsl150 string - //go:embed zblit.vert.0.dxbc - zblit_vert_0_dxbc string - //go:embed zblit.vert.0.metallibmacos - zblit_vert_0_metallibmacos string - //go:embed zblit.vert.0.metallibios - zblit_vert_0_metallibios string - //go:embed zblit.vert.0.metallibiossimulator - zblit_vert_0_metallibiossimulator string - Shader_copy_frag = shader.Sources{ - Name: "copy.frag", - Inputs: []shader.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, - Textures: []shader.TextureBinding{{Name: "tex", Binding: 0}}, - } - //go:embed zcopy.frag.0.spirv - zcopy_frag_0_spirv string - //go:embed zcopy.frag.0.glsl100es - zcopy_frag_0_glsl100es string - //go:embed zcopy.frag.0.glsl150 - zcopy_frag_0_glsl150 string - //go:embed zcopy.frag.0.dxbc - zcopy_frag_0_dxbc string - //go:embed zcopy.frag.0.metallibmacos - zcopy_frag_0_metallibmacos string - //go:embed zcopy.frag.0.metallibios - zcopy_frag_0_metallibios string - //go:embed zcopy.frag.0.metallibiossimulator - zcopy_frag_0_metallibiossimulator string - Shader_copy_vert = shader.Sources{ - Name: "copy.vert", - Inputs: []shader.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_block.scale", Type: 0x0, Size: 2, Offset: 0}, {Name: "_block.pos", Type: 0x0, Size: 2, Offset: 8}, {Name: "_block.uvScale", Type: 0x0, Size: 2, Offset: 16}}, - Size: 24, - }, - } - //go:embed zcopy.vert.0.spirv - zcopy_vert_0_spirv string - //go:embed zcopy.vert.0.glsl100es - zcopy_vert_0_glsl100es string - //go:embed zcopy.vert.0.glsl150 - zcopy_vert_0_glsl150 string - //go:embed zcopy.vert.0.dxbc - zcopy_vert_0_dxbc string - //go:embed zcopy.vert.0.metallibmacos - zcopy_vert_0_metallibmacos string - //go:embed zcopy.vert.0.metallibios - zcopy_vert_0_metallibios string - //go:embed zcopy.vert.0.metallibiossimulator - zcopy_vert_0_metallibiossimulator string - Shader_cover_frag = [...]shader.Sources{ - { - Name: "cover.frag", - Inputs: []shader.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_color.color", Type: 0x0, Size: 4, Offset: 112}}, - Size: 16, - }, - Textures: []shader.TextureBinding{{Name: "cover", Binding: 1}}, - }, - { - Name: "cover.frag", - Inputs: []shader.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_gradient.color1", Type: 0x0, Size: 4, Offset: 96}, {Name: "_gradient.color2", Type: 0x0, Size: 4, Offset: 112}}, - Size: 32, - }, - Textures: []shader.TextureBinding{{Name: "cover", Binding: 1}}, - }, - { - Name: "cover.frag", - Inputs: []shader.InputLocation{{Name: "vCoverUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vUV", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, - Textures: []shader.TextureBinding{{Name: "tex", Binding: 0}, {Name: "cover", Binding: 1}}, - }, - } - //go:embed zcover.frag.0.spirv - zcover_frag_0_spirv string - //go:embed zcover.frag.0.glsl100es - zcover_frag_0_glsl100es string - //go:embed zcover.frag.0.glsl150 - zcover_frag_0_glsl150 string - //go:embed zcover.frag.0.dxbc - zcover_frag_0_dxbc string - //go:embed zcover.frag.0.metallibmacos - zcover_frag_0_metallibmacos string - //go:embed zcover.frag.0.metallibios - zcover_frag_0_metallibios string - //go:embed zcover.frag.0.metallibiossimulator - zcover_frag_0_metallibiossimulator string - //go:embed zcover.frag.1.spirv - zcover_frag_1_spirv string - //go:embed zcover.frag.1.glsl100es - zcover_frag_1_glsl100es string - //go:embed zcover.frag.1.glsl150 - zcover_frag_1_glsl150 string - //go:embed zcover.frag.1.dxbc - zcover_frag_1_dxbc string - //go:embed zcover.frag.1.metallibmacos - zcover_frag_1_metallibmacos string - //go:embed zcover.frag.1.metallibios - zcover_frag_1_metallibios string - //go:embed zcover.frag.1.metallibiossimulator - zcover_frag_1_metallibiossimulator string - //go:embed zcover.frag.2.spirv - zcover_frag_2_spirv string - //go:embed zcover.frag.2.glsl100es - zcover_frag_2_glsl100es string - //go:embed zcover.frag.2.glsl150 - zcover_frag_2_glsl150 string - //go:embed zcover.frag.2.dxbc - zcover_frag_2_dxbc string - //go:embed zcover.frag.2.metallibmacos - zcover_frag_2_metallibmacos string - //go:embed zcover.frag.2.metallibios - zcover_frag_2_metallibios string - //go:embed zcover.frag.2.metallibiossimulator - zcover_frag_2_metallibiossimulator string - Shader_cover_vert = shader.Sources{ - Name: "cover.vert", - Inputs: []shader.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.uvCoverTransform", Type: 0x0, Size: 4, Offset: 16}, {Name: "_block.uvTransformR1", Type: 0x0, Size: 4, Offset: 32}, {Name: "_block.uvTransformR2", Type: 0x0, Size: 4, Offset: 48}, {Name: "_block.fbo", Type: 0x0, Size: 1, Offset: 64}}, - Size: 68, - }, - } - //go:embed zcover.vert.0.spirv - zcover_vert_0_spirv string - //go:embed zcover.vert.0.glsl100es - zcover_vert_0_glsl100es string - //go:embed zcover.vert.0.glsl150 - zcover_vert_0_glsl150 string - //go:embed zcover.vert.0.dxbc - zcover_vert_0_dxbc string - //go:embed zcover.vert.0.metallibmacos - zcover_vert_0_metallibmacos string - //go:embed zcover.vert.0.metallibios - zcover_vert_0_metallibios string - //go:embed zcover.vert.0.metallibiossimulator - zcover_vert_0_metallibiossimulator string - Shader_input_vert = shader.Sources{ - Name: "input.vert", - Inputs: []shader.InputLocation{{Name: "position", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 4}}, - } - //go:embed zinput.vert.0.spirv - zinput_vert_0_spirv string - //go:embed zinput.vert.0.glsl100es - zinput_vert_0_glsl100es string - //go:embed zinput.vert.0.glsl150 - zinput_vert_0_glsl150 string - //go:embed zinput.vert.0.dxbc - zinput_vert_0_dxbc string - //go:embed zinput.vert.0.metallibmacos - zinput_vert_0_metallibmacos string - //go:embed zinput.vert.0.metallibios - zinput_vert_0_metallibios string - //go:embed zinput.vert.0.metallibiossimulator - zinput_vert_0_metallibiossimulator string - Shader_intersect_frag = shader.Sources{ - Name: "intersect.frag", - Inputs: []shader.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, - Textures: []shader.TextureBinding{{Name: "cover", Binding: 0}}, - } - //go:embed zintersect.frag.0.spirv - zintersect_frag_0_spirv string - //go:embed zintersect.frag.0.glsl100es - zintersect_frag_0_glsl100es string - //go:embed zintersect.frag.0.glsl150 - zintersect_frag_0_glsl150 string - //go:embed zintersect.frag.0.dxbc - zintersect_frag_0_dxbc string - //go:embed zintersect.frag.0.metallibmacos - zintersect_frag_0_metallibmacos string - //go:embed zintersect.frag.0.metallibios - zintersect_frag_0_metallibios string - //go:embed zintersect.frag.0.metallibiossimulator - zintersect_frag_0_metallibiossimulator string - Shader_intersect_vert = shader.Sources{ - Name: "intersect.vert", - Inputs: []shader.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_block.uvTransform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.subUVTransform", Type: 0x0, Size: 4, Offset: 16}}, - Size: 32, - }, - } - //go:embed zintersect.vert.0.spirv - zintersect_vert_0_spirv string - //go:embed zintersect.vert.0.glsl100es - zintersect_vert_0_glsl100es string - //go:embed zintersect.vert.0.glsl150 - zintersect_vert_0_glsl150 string - //go:embed zintersect.vert.0.dxbc - zintersect_vert_0_dxbc string - //go:embed zintersect.vert.0.metallibmacos - zintersect_vert_0_metallibmacos string - //go:embed zintersect.vert.0.metallibios - zintersect_vert_0_metallibios string - //go:embed zintersect.vert.0.metallibiossimulator - zintersect_vert_0_metallibiossimulator string - Shader_material_frag = shader.Sources{ - Name: "material.frag", - Inputs: []shader.InputLocation{{Name: "vUV", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_color.emulateSRGB", Type: 0x0, Size: 1, Offset: 16}}, - Size: 4, - }, - Textures: []shader.TextureBinding{{Name: "tex", Binding: 0}}, - } - //go:embed zmaterial.frag.0.spirv - zmaterial_frag_0_spirv string - //go:embed zmaterial.frag.0.glsl100es - zmaterial_frag_0_glsl100es string - //go:embed zmaterial.frag.0.glsl150 - zmaterial_frag_0_glsl150 string - //go:embed zmaterial.frag.0.dxbc - zmaterial_frag_0_dxbc string - //go:embed zmaterial.frag.0.metallibmacos - zmaterial_frag_0_metallibmacos string - //go:embed zmaterial.frag.0.metallibios - zmaterial_frag_0_metallibios string - //go:embed zmaterial.frag.0.metallibiossimulator - zmaterial_frag_0_metallibiossimulator string - Shader_material_vert = shader.Sources{ - Name: "material.vert", - Inputs: []shader.InputLocation{{Name: "pos", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "uv", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_block.scale", Type: 0x0, Size: 2, Offset: 0}, {Name: "_block.pos", Type: 0x0, Size: 2, Offset: 8}}, - Size: 16, - }, - } - //go:embed zmaterial.vert.0.spirv - zmaterial_vert_0_spirv string - //go:embed zmaterial.vert.0.glsl100es - zmaterial_vert_0_glsl100es string - //go:embed zmaterial.vert.0.glsl150 - zmaterial_vert_0_glsl150 string - //go:embed zmaterial.vert.0.dxbc - zmaterial_vert_0_dxbc string - //go:embed zmaterial.vert.0.metallibmacos - zmaterial_vert_0_metallibmacos string - //go:embed zmaterial.vert.0.metallibios - zmaterial_vert_0_metallibios string - //go:embed zmaterial.vert.0.metallibiossimulator - zmaterial_vert_0_metallibiossimulator string - Shader_simple_frag = shader.Sources{ - Name: "simple.frag", - } - //go:embed zsimple.frag.0.spirv - zsimple_frag_0_spirv string - //go:embed zsimple.frag.0.glsl100es - zsimple_frag_0_glsl100es string - //go:embed zsimple.frag.0.glsl150 - zsimple_frag_0_glsl150 string - //go:embed zsimple.frag.0.dxbc - zsimple_frag_0_dxbc string - //go:embed zsimple.frag.0.metallibmacos - zsimple_frag_0_metallibmacos string - //go:embed zsimple.frag.0.metallibios - zsimple_frag_0_metallibios string - //go:embed zsimple.frag.0.metallibiossimulator - zsimple_frag_0_metallibiossimulator string - Shader_stencil_frag = shader.Sources{ - Name: "stencil.frag", - Inputs: []shader.InputLocation{{Name: "vFrom", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 2}, {Name: "vCtrl", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 2}, {Name: "vTo", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}}, - } - //go:embed zstencil.frag.0.spirv - zstencil_frag_0_spirv string - //go:embed zstencil.frag.0.glsl100es - zstencil_frag_0_glsl100es string - //go:embed zstencil.frag.0.glsl150 - zstencil_frag_0_glsl150 string - //go:embed zstencil.frag.0.dxbc - zstencil_frag_0_dxbc string - //go:embed zstencil.frag.0.metallibmacos - zstencil_frag_0_metallibmacos string - //go:embed zstencil.frag.0.metallibios - zstencil_frag_0_metallibios string - //go:embed zstencil.frag.0.metallibiossimulator - zstencil_frag_0_metallibiossimulator string - Shader_stencil_vert = shader.Sources{ - Name: "stencil.vert", - Inputs: []shader.InputLocation{{Name: "corner", Location: 0, Semantic: "TEXCOORD", SemanticIndex: 0, Type: 0x0, Size: 1}, {Name: "maxy", Location: 1, Semantic: "TEXCOORD", SemanticIndex: 1, Type: 0x0, Size: 1}, {Name: "from", Location: 2, Semantic: "TEXCOORD", SemanticIndex: 2, Type: 0x0, Size: 2}, {Name: "ctrl", Location: 3, Semantic: "TEXCOORD", SemanticIndex: 3, Type: 0x0, Size: 2}, {Name: "to", Location: 4, Semantic: "TEXCOORD", SemanticIndex: 4, Type: 0x0, Size: 2}}, - Uniforms: shader.UniformsReflection{ - Locations: []shader.UniformLocation{{Name: "_block.transform", Type: 0x0, Size: 4, Offset: 0}, {Name: "_block.pathOffset", Type: 0x0, Size: 2, Offset: 16}}, - Size: 24, - }, - } - //go:embed zstencil.vert.0.spirv - zstencil_vert_0_spirv string - //go:embed zstencil.vert.0.glsl100es - zstencil_vert_0_glsl100es string - //go:embed zstencil.vert.0.glsl150 - zstencil_vert_0_glsl150 string - //go:embed zstencil.vert.0.dxbc - zstencil_vert_0_dxbc string - //go:embed zstencil.vert.0.metallibmacos - zstencil_vert_0_metallibmacos string - //go:embed zstencil.vert.0.metallibios - zstencil_vert_0_metallibios string - //go:embed zstencil.vert.0.metallibiossimulator - zstencil_vert_0_metallibiossimulator string -) - -func init() { - const ( - opengles = runtime.GOOS == "linux" || runtime.GOOS == "freebsd" || runtime.GOOS == "openbsd" || runtime.GOOS == "windows" || runtime.GOOS == "js" || runtime.GOOS == "android" || runtime.GOOS == "darwin" || runtime.GOOS == "ios" - opengl = runtime.GOOS == "darwin" - d3d11 = runtime.GOOS == "windows" - vulkan = runtime.GOOS == "linux" || runtime.GOOS == "android" - ) - if vulkan { - Shader_blit_frag[0].SPIRV = zblit_frag_0_spirv - } - if opengles { - Shader_blit_frag[0].GLSL100ES = zblit_frag_0_glsl100es - } - if opengl { - Shader_blit_frag[0].GLSL150 = zblit_frag_0_glsl150 - } - if d3d11 { - Shader_blit_frag[0].DXBC = zblit_frag_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_blit_frag[0].MetalLib = zblit_frag_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_blit_frag[0].MetalLib = zblit_frag_0_metallibiossimulator - } else { - Shader_blit_frag[0].MetalLib = zblit_frag_0_metallibios - } - } - if vulkan { - Shader_blit_frag[1].SPIRV = zblit_frag_1_spirv - } - if opengles { - Shader_blit_frag[1].GLSL100ES = zblit_frag_1_glsl100es - } - if opengl { - Shader_blit_frag[1].GLSL150 = zblit_frag_1_glsl150 - } - if d3d11 { - Shader_blit_frag[1].DXBC = zblit_frag_1_dxbc - } - if runtime.GOOS == "darwin" { - Shader_blit_frag[1].MetalLib = zblit_frag_1_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_blit_frag[1].MetalLib = zblit_frag_1_metallibiossimulator - } else { - Shader_blit_frag[1].MetalLib = zblit_frag_1_metallibios - } - } - if vulkan { - Shader_blit_frag[2].SPIRV = zblit_frag_2_spirv - } - if opengles { - Shader_blit_frag[2].GLSL100ES = zblit_frag_2_glsl100es - } - if opengl { - Shader_blit_frag[2].GLSL150 = zblit_frag_2_glsl150 - } - if d3d11 { - Shader_blit_frag[2].DXBC = zblit_frag_2_dxbc - } - if runtime.GOOS == "darwin" { - Shader_blit_frag[2].MetalLib = zblit_frag_2_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_blit_frag[2].MetalLib = zblit_frag_2_metallibiossimulator - } else { - Shader_blit_frag[2].MetalLib = zblit_frag_2_metallibios - } - } - if vulkan { - Shader_blit_vert.SPIRV = zblit_vert_0_spirv - } - if opengles { - Shader_blit_vert.GLSL100ES = zblit_vert_0_glsl100es - } - if opengl { - Shader_blit_vert.GLSL150 = zblit_vert_0_glsl150 - } - if d3d11 { - Shader_blit_vert.DXBC = zblit_vert_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_blit_vert.MetalLib = zblit_vert_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_blit_vert.MetalLib = zblit_vert_0_metallibiossimulator - } else { - Shader_blit_vert.MetalLib = zblit_vert_0_metallibios - } - } - if vulkan { - Shader_copy_frag.SPIRV = zcopy_frag_0_spirv - } - if opengles { - Shader_copy_frag.GLSL100ES = zcopy_frag_0_glsl100es - } - if opengl { - Shader_copy_frag.GLSL150 = zcopy_frag_0_glsl150 - } - if d3d11 { - Shader_copy_frag.DXBC = zcopy_frag_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_copy_frag.MetalLib = zcopy_frag_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_copy_frag.MetalLib = zcopy_frag_0_metallibiossimulator - } else { - Shader_copy_frag.MetalLib = zcopy_frag_0_metallibios - } - } - if vulkan { - Shader_copy_vert.SPIRV = zcopy_vert_0_spirv - } - if opengles { - Shader_copy_vert.GLSL100ES = zcopy_vert_0_glsl100es - } - if opengl { - Shader_copy_vert.GLSL150 = zcopy_vert_0_glsl150 - } - if d3d11 { - Shader_copy_vert.DXBC = zcopy_vert_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_copy_vert.MetalLib = zcopy_vert_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_copy_vert.MetalLib = zcopy_vert_0_metallibiossimulator - } else { - Shader_copy_vert.MetalLib = zcopy_vert_0_metallibios - } - } - if vulkan { - Shader_cover_frag[0].SPIRV = zcover_frag_0_spirv - } - if opengles { - Shader_cover_frag[0].GLSL100ES = zcover_frag_0_glsl100es - } - if opengl { - Shader_cover_frag[0].GLSL150 = zcover_frag_0_glsl150 - } - if d3d11 { - Shader_cover_frag[0].DXBC = zcover_frag_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_cover_frag[0].MetalLib = zcover_frag_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_cover_frag[0].MetalLib = zcover_frag_0_metallibiossimulator - } else { - Shader_cover_frag[0].MetalLib = zcover_frag_0_metallibios - } - } - if vulkan { - Shader_cover_frag[1].SPIRV = zcover_frag_1_spirv - } - if opengles { - Shader_cover_frag[1].GLSL100ES = zcover_frag_1_glsl100es - } - if opengl { - Shader_cover_frag[1].GLSL150 = zcover_frag_1_glsl150 - } - if d3d11 { - Shader_cover_frag[1].DXBC = zcover_frag_1_dxbc - } - if runtime.GOOS == "darwin" { - Shader_cover_frag[1].MetalLib = zcover_frag_1_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_cover_frag[1].MetalLib = zcover_frag_1_metallibiossimulator - } else { - Shader_cover_frag[1].MetalLib = zcover_frag_1_metallibios - } - } - if vulkan { - Shader_cover_frag[2].SPIRV = zcover_frag_2_spirv - } - if opengles { - Shader_cover_frag[2].GLSL100ES = zcover_frag_2_glsl100es - } - if opengl { - Shader_cover_frag[2].GLSL150 = zcover_frag_2_glsl150 - } - if d3d11 { - Shader_cover_frag[2].DXBC = zcover_frag_2_dxbc - } - if runtime.GOOS == "darwin" { - Shader_cover_frag[2].MetalLib = zcover_frag_2_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_cover_frag[2].MetalLib = zcover_frag_2_metallibiossimulator - } else { - Shader_cover_frag[2].MetalLib = zcover_frag_2_metallibios - } - } - if vulkan { - Shader_cover_vert.SPIRV = zcover_vert_0_spirv - } - if opengles { - Shader_cover_vert.GLSL100ES = zcover_vert_0_glsl100es - } - if opengl { - Shader_cover_vert.GLSL150 = zcover_vert_0_glsl150 - } - if d3d11 { - Shader_cover_vert.DXBC = zcover_vert_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_cover_vert.MetalLib = zcover_vert_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_cover_vert.MetalLib = zcover_vert_0_metallibiossimulator - } else { - Shader_cover_vert.MetalLib = zcover_vert_0_metallibios - } - } - if vulkan { - Shader_input_vert.SPIRV = zinput_vert_0_spirv - } - if opengles { - Shader_input_vert.GLSL100ES = zinput_vert_0_glsl100es - } - if opengl { - Shader_input_vert.GLSL150 = zinput_vert_0_glsl150 - } - if d3d11 { - Shader_input_vert.DXBC = zinput_vert_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_input_vert.MetalLib = zinput_vert_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_input_vert.MetalLib = zinput_vert_0_metallibiossimulator - } else { - Shader_input_vert.MetalLib = zinput_vert_0_metallibios - } - } - if vulkan { - Shader_intersect_frag.SPIRV = zintersect_frag_0_spirv - } - if opengles { - Shader_intersect_frag.GLSL100ES = zintersect_frag_0_glsl100es - } - if opengl { - Shader_intersect_frag.GLSL150 = zintersect_frag_0_glsl150 - } - if d3d11 { - Shader_intersect_frag.DXBC = zintersect_frag_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_intersect_frag.MetalLib = zintersect_frag_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_intersect_frag.MetalLib = zintersect_frag_0_metallibiossimulator - } else { - Shader_intersect_frag.MetalLib = zintersect_frag_0_metallibios - } - } - if vulkan { - Shader_intersect_vert.SPIRV = zintersect_vert_0_spirv - } - if opengles { - Shader_intersect_vert.GLSL100ES = zintersect_vert_0_glsl100es - } - if opengl { - Shader_intersect_vert.GLSL150 = zintersect_vert_0_glsl150 - } - if d3d11 { - Shader_intersect_vert.DXBC = zintersect_vert_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_intersect_vert.MetalLib = zintersect_vert_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_intersect_vert.MetalLib = zintersect_vert_0_metallibiossimulator - } else { - Shader_intersect_vert.MetalLib = zintersect_vert_0_metallibios - } - } - if vulkan { - Shader_material_frag.SPIRV = zmaterial_frag_0_spirv - } - if opengles { - Shader_material_frag.GLSL100ES = zmaterial_frag_0_glsl100es - } - if opengl { - Shader_material_frag.GLSL150 = zmaterial_frag_0_glsl150 - } - if d3d11 { - Shader_material_frag.DXBC = zmaterial_frag_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_material_frag.MetalLib = zmaterial_frag_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_material_frag.MetalLib = zmaterial_frag_0_metallibiossimulator - } else { - Shader_material_frag.MetalLib = zmaterial_frag_0_metallibios - } - } - if vulkan { - Shader_material_vert.SPIRV = zmaterial_vert_0_spirv - } - if opengles { - Shader_material_vert.GLSL100ES = zmaterial_vert_0_glsl100es - } - if opengl { - Shader_material_vert.GLSL150 = zmaterial_vert_0_glsl150 - } - if d3d11 { - Shader_material_vert.DXBC = zmaterial_vert_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_material_vert.MetalLib = zmaterial_vert_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_material_vert.MetalLib = zmaterial_vert_0_metallibiossimulator - } else { - Shader_material_vert.MetalLib = zmaterial_vert_0_metallibios - } - } - if vulkan { - Shader_simple_frag.SPIRV = zsimple_frag_0_spirv - } - if opengles { - Shader_simple_frag.GLSL100ES = zsimple_frag_0_glsl100es - } - if opengl { - Shader_simple_frag.GLSL150 = zsimple_frag_0_glsl150 - } - if d3d11 { - Shader_simple_frag.DXBC = zsimple_frag_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_simple_frag.MetalLib = zsimple_frag_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_simple_frag.MetalLib = zsimple_frag_0_metallibiossimulator - } else { - Shader_simple_frag.MetalLib = zsimple_frag_0_metallibios - } - } - if vulkan { - Shader_stencil_frag.SPIRV = zstencil_frag_0_spirv - } - if opengles { - Shader_stencil_frag.GLSL100ES = zstencil_frag_0_glsl100es - } - if opengl { - Shader_stencil_frag.GLSL150 = zstencil_frag_0_glsl150 - } - if d3d11 { - Shader_stencil_frag.DXBC = zstencil_frag_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_stencil_frag.MetalLib = zstencil_frag_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_stencil_frag.MetalLib = zstencil_frag_0_metallibiossimulator - } else { - Shader_stencil_frag.MetalLib = zstencil_frag_0_metallibios - } - } - if vulkan { - Shader_stencil_vert.SPIRV = zstencil_vert_0_spirv - } - if opengles { - Shader_stencil_vert.GLSL100ES = zstencil_vert_0_glsl100es - } - if opengl { - Shader_stencil_vert.GLSL150 = zstencil_vert_0_glsl150 - } - if d3d11 { - Shader_stencil_vert.DXBC = zstencil_vert_0_dxbc - } - if runtime.GOOS == "darwin" { - Shader_stencil_vert.MetalLib = zstencil_vert_0_metallibmacos - } - if runtime.GOOS == "ios" { - if runtime.GOARCH == "amd64" { - Shader_stencil_vert.MetalLib = zstencil_vert_0_metallibiossimulator - } else { - Shader_stencil_vert.MetalLib = zstencil_vert_0_metallibios - } - } -} diff --git a/gio/shader/shader.go b/gio/shader/shader.go deleted file mode 100644 index e1263c1..0000000 --- a/gio/shader/shader.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package shader - -type Sources struct { - Name string - SPIRV string - GLSL100ES string - GLSL150 string - DXBC string - MetalLib string - Uniforms UniformsReflection - Inputs []InputLocation - Textures []TextureBinding - StorageBuffers []BufferBinding - Images []ImageBinding - WorkgroupSize [3]int -} - -type UniformsReflection struct { - Locations []UniformLocation - Size int -} - -type ImageBinding struct { - Name string - Binding int -} - -type BufferBinding struct { - Name string - Binding int -} - -type TextureBinding struct { - Name string - Binding int -} - -type UniformLocation struct { - Name string - Type DataType - Size int - Offset int -} - -type InputLocation struct { - // For GLSL. - Name string - Location int - // For HLSL. - Semantic string - SemanticIndex int - - Type DataType - Size int -} - -type DataType uint8 - -const ( - DataTypeFloat DataType = iota - DataTypeInt - DataTypeShort -) diff --git a/gio/text/family_parser.go b/gio/text/family_parser.go deleted file mode 100644 index 544235b..0000000 --- a/gio/text/family_parser.go +++ /dev/null @@ -1,246 +0,0 @@ -package text - -import ( - "fmt" - "strings" - "unicode" - "unicode/utf8" -) - -type tokenKind uint8 - -const ( - tokenStr tokenKind = iota - tokenComma - tokenEOF -) - -type token struct { - kind tokenKind - value string -} - -func (t token) String() string { - switch t.kind { - case tokenStr: - return t.value - case tokenComma: - return "," - case tokenEOF: - return "EOF" - default: - return "unknown" - } -} - -type lexState func(*lexer) lexState - -func lexText(l *lexer) lexState { - for { - switch r := l.next(); { - case r == -1: - l.ignore() - l.emit(tokenEOF) - return nil - case unicode.IsSpace(r): - continue - case r == ',': - l.ignore() - l.emit(tokenComma) - case r == '"': - l.ignore() - return lexDquote - case r == '\'': - l.ignore() - return lexSquote - default: - return lexBareStr - } - } -} - -func lexBareStr(l *lexer) lexState { - defer l.emitProcessed(tokenStr, func(s string) (string, error) { - return strings.TrimSpace(s), nil - }) - for { - if strings.HasPrefix(l.input[l.pos:], `,`) { - return lexText - } - switch r := l.next(); { - case r == -1: - return lexText - } - } -} - -func lexDquote(l *lexer) lexState { - return lexQuote(l, `"`) -} - -func lexSquote(l *lexer) lexState { - return lexQuote(l, `'`) -} - -func unescape(s string, quote rune) (string, error) { - var b strings.Builder - hitNonSpace := false - var wb strings.Builder - for i := 0; i < len(s); { - r, sz := utf8.DecodeRuneInString(s[i:]) - i += sz - if unicode.IsSpace(r) { - if !hitNonSpace { - continue - } - wb.WriteRune(r) - continue - } - hitNonSpace = true - // If we get here, we're not looking at whitespace. - // Insert any buffered up whitespace characters from - // the gap between words. - b.WriteString(wb.String()) - wb.Reset() - if r == '\\' { - r, sz := utf8.DecodeRuneInString(s[i:]) - i += sz - switch r { - case '\\', quote: - b.WriteRune(r) - default: - return "", fmt.Errorf("illegal escape sequence \\%c", r) - } - } else { - b.WriteRune(r) - } - } - return b.String(), nil -} - -func lexQuote(l *lexer, mark string) lexState { - escaping := false - for { - if isQuote := strings.HasPrefix(l.input[l.pos:], mark); isQuote && !escaping { - err := l.emitProcessed(tokenStr, func(s string) (string, error) { - return unescape(s, []rune(mark)[0]) - }) - if err != nil { - l.err = err - return nil - } - l.next() - l.ignore() - return lexText - } - escaped := escaping - switch r := l.next(); { - case r == -1: - l.err = fmt.Errorf("unexpected EOF while parsing %s-quoted family", mark) - return lexText - case r == '\\': - if !escaped { - escaping = true - } - } - if escaped { - escaping = false - } - } -} - -type lexer struct { - input string - pos int - tokens []token - err error -} - -func (l *lexer) ignore() { - l.input = l.input[l.pos:] - l.pos = 0 -} - -// next decodes the next rune in the input and returns it. -func (l *lexer) next() int32 { - if l.pos >= len(l.input) { - return -1 - } - r, w := utf8.DecodeRuneInString(l.input[l.pos:]) - l.pos += w - return r -} - -// emit adds a token of the given kind. -func (l *lexer) emit(t tokenKind) { - l.emitProcessed(t, func(s string) (string, error) { return s, nil }) -} - -// emitProcessed adds a token of the given kind, but transforms its value -// with the provided closure first. -func (l *lexer) emitProcessed(t tokenKind, f func(string) (string, error)) error { - val, err := f(l.input[:l.pos]) - l.tokens = append(l.tokens, token{ - kind: t, - value: val, - }) - l.ignore() - return err -} - -// run executes the lexer on the given input. -func (l *lexer) run(input string) ([]token, error) { - l.input = input - l.tokens = l.tokens[:0] - l.pos = 0 - for state := lexText; state != nil; { - state = state(l) - } - return l.tokens, l.err -} - -// parser implements a simple recursive descent parser for font family fallback -// expressions. -type parser struct { - faces []string - lexer lexer - tokens []token -} - -// parse the provided rule and return the extracted font families. The returned families -// are valid only until the next call to parse. If parsing fails, an error describing the -// failure is returned instead. -func (p *parser) parse(rule string) ([]string, error) { - var err error - p.tokens, err = p.lexer.run(rule) - if err != nil { - return nil, err - } - p.faces = p.faces[:0] - return p.faces, p.parseList() -} - -// parse implements the production: -// -// LIST ::= | -func (p *parser) parseList() error { - if len(p.tokens) < 0 { - return fmt.Errorf("expected family name, got EOF") - } - if head := p.tokens[0]; head.kind != tokenStr { - return fmt.Errorf("expected family name, got %s", head) - } else { - p.faces = append(p.faces, head.value) - p.tokens = p.tokens[1:] - } - - switch head := p.tokens[0]; head.kind { - case tokenEOF: - return nil - case tokenComma: - p.tokens = p.tokens[1:] - return p.parseList() - default: - return fmt.Errorf("unexpected token %s", head) - } -} diff --git a/gio/text/family_parser_test.go b/gio/text/family_parser_test.go deleted file mode 100644 index 6c58b2f..0000000 --- a/gio/text/family_parser_test.go +++ /dev/null @@ -1,178 +0,0 @@ -package text - -import ( - "slices" - "testing" -) - -func TestParser(t *testing.T) { - type scenario struct { - variantName string - input string - } - type testcase struct { - name string - inputs []scenario - expected []string - shouldErr bool - } - - for _, tc := range []testcase{ - { - name: "empty", - inputs: []scenario{ - { - variantName: "", - }, - }, - shouldErr: true, - }, - { - name: "comma failure", - inputs: []scenario{ - { - variantName: "bare single", - input: ",", - }, - { - variantName: "bare multiple", - input: ",, ,,", - }, - }, - shouldErr: true, - }, - { - name: "comma success", - inputs: []scenario{ - { - variantName: "squote", - input: "','", - }, - { - variantName: "dquote", - input: `","`, - }, - }, - expected: []string{","}, - }, - { - name: "comma success multiple", - inputs: []scenario{ - { - variantName: "squote", - input: "',,', ',,'", - }, - { - variantName: "dquote", - input: `",,", ",,"`, - }, - }, - expected: []string{",,", ",,"}, - }, - { - name: "backslashes", - inputs: []scenario{ - { - variantName: "bare", - input: `\font\\`, - }, - { - variantName: "dquote", - input: `"\\font\\\\"`, - }, - { - variantName: "squote", - input: `'\\font\\\\'`, - }, - }, - expected: []string{`\font\\`}, - }, - { - name: "invalid backslashes", - inputs: []scenario{ - { - variantName: "dquote", - input: `"\\""`, - }, - { - variantName: "squote", - input: `'\\''`, - }, - }, - shouldErr: true, - }, - { - name: "too many quotes", - inputs: []scenario{ - { - variantName: "dquote", - input: `"""`, - }, - { - variantName: "squote", - input: `'''`, - }, - }, - shouldErr: true, - }, - { - name: "serif serif's serif\"s", - inputs: []scenario{ - { - variantName: "bare", - input: `serif, serif's, serif"s`, - }, - { - variantName: "squote", - input: `'serif', 'serif\'s', 'serif"s'`, - }, - { - variantName: "dquote", - input: `"serif", "serif's", "serif\"s"`, - }, - }, - expected: []string{"serif", `serif's`, `serif"s`}, - }, - { - name: "complex list", - inputs: []scenario{ - { - variantName: "bare", - input: `Times New Roman, Georgia Common, Helvetica Neue, serif`, - }, - { - variantName: "squote", - input: `'Times New Roman', 'Georgia Common', 'Helvetica Neue', 'serif'`, - }, - { - variantName: "dquote", - input: `"Times New Roman", "Georgia Common", "Helvetica Neue", "serif"`, - }, - { - variantName: "mixed", - input: `Times New Roman, "Georgia Common", 'Helvetica Neue', "serif"`, - }, - { - variantName: "mixed with weird spacing", - input: `Times New Roman ,"Georgia Common" , 'Helvetica Neue' ,"serif"`, - }, - }, - expected: []string{"Times New Roman", "Georgia Common", "Helvetica Neue", "serif"}, - }, - } { - t.Run(tc.name, func(t *testing.T) { - var p parser - for _, scen := range tc.inputs { - t.Run(scen.variantName, func(t *testing.T) { - actual, err := p.parse(scen.input) - if (err != nil) != tc.shouldErr { - t.Errorf("unexpected error state: %v", err) - } - if !slices.Equal(tc.expected, actual) { - t.Errorf("expected\n%q\ngot\n%q", tc.expected, actual) - } - }) - } - }) - } -} diff --git a/gio/text/gotext.go b/gio/text/gotext.go deleted file mode 100644 index b8474b7..0000000 --- a/gio/text/gotext.go +++ /dev/null @@ -1,913 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package text - -import ( - "bytes" - "fmt" - "image" - "io" - "log" - "os" - "slices" - - "github.com/go-text/typesetting/di" - "github.com/go-text/typesetting/font" - gotextot "github.com/go-text/typesetting/font/opentype" - "github.com/go-text/typesetting/fontscan" - "github.com/go-text/typesetting/language" - "github.com/go-text/typesetting/shaping" - "golang.org/x/image/math/fixed" - "golang.org/x/text/unicode/bidi" - - "github.com/p9c/p9/pkg/gel/gio/f32" - giofont "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/font/opentype" - "github.com/p9c/p9/pkg/gel/gio/internal/debug" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -// document holds a collection of shaped lines and alignment information for -// those lines. -type document struct { - lines []line - alignment Alignment - // alignWidth is the width used when aligning text. - alignWidth int - unreadRuneCount int -} - -// append adds the lines of other to the end of l and ensures they -// are aligned to the same width. -func (l *document) append(other document) { - l.lines = append(l.lines, other.lines...) - l.alignWidth = max(l.alignWidth, other.alignWidth) - calculateYOffsets(l.lines) -} - -// reset empties the document in preparation to reuse its memory. -func (l *document) reset() { - l.lines = l.lines[:0] - l.alignment = Start - l.alignWidth = 0 - l.unreadRuneCount = 0 -} - -// A line contains the measurements of a line of text. -type line struct { - // runs contains sequences of shaped glyphs with common attributes. The order - // of runs is logical, meaning that the first run will contain the glyphs - // corresponding to the first runes of data in the original text. - runs []runLayout - // visualOrder is a slice of indices into Runs that describes the visual positions - // of each run of text. Iterating this slice and accessing Runs at each - // of the values stored in this slice traverses the runs in proper visual - // order from left to right. - visualOrder []int - // width is the width of the line. - width fixed.Int26_6 - // ascent is the height above the baseline. - ascent fixed.Int26_6 - // descent is the height below the baseline, including - // the line gap. - descent fixed.Int26_6 - // lineHeight captures the gap that should exist between the baseline of this - // line and the previous (if any). - lineHeight fixed.Int26_6 - // direction is the dominant direction of the line. This direction will be - // used to align the text content of the line, but may not match the actual - // direction of the runs of text within the line (such as an RTL sentence - // within an LTR paragraph). - direction system.TextDirection - // runeCount is the number of text runes represented by this line's runs. - runeCount int - - yOffset int -} - -// insertTrailingSyntheticNewline adds a synthetic newline to the final logical run of the line -// with the given shaping cluster index. -func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) { - // If there was a newline at the end of this paragraph, insert a synthetic glyph representing it. - finalContentRun := len(l.runs) - 1 - // If there was a trailing newline update the rune counts to include - // it on the last line of the paragraph. - l.runeCount += 1 - l.runs[finalContentRun].Runes.Count += 1 - - syntheticGlyph := glyph{ - id: 0, - clusterIndex: newLineClusterIdx, - glyphCount: 0, - runeCount: 1, - xAdvance: 0, - yAdvance: 0, - xOffset: 0, - yOffset: 0, - } - // Inset the synthetic newline glyph on the proper end of the run. - if l.runs[finalContentRun].Direction.Progression() == system.FromOrigin { - l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, syntheticGlyph) - } else { - // Ensure capacity. - l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, glyph{}) - copy(l.runs[finalContentRun].Glyphs[1:], l.runs[finalContentRun].Glyphs) - l.runs[finalContentRun].Glyphs[0] = syntheticGlyph - } -} - -func (l *line) setTruncatedCount(truncatedCount int) { - // If we've truncated the text with a truncator, adjust the rune counts within the - // truncator to make it represent the truncated text. - finalRunIdx := len(l.runs) - 1 - l.runs[finalRunIdx].truncator = true - finalGlyphIdx := len(l.runs[finalRunIdx].Glyphs) - 1 - // The run represents all of the truncated text. - l.runs[finalRunIdx].Runes.Count = truncatedCount - // Only the final glyph represents any runes, and it represents all truncated text. - for i := range l.runs[finalRunIdx].Glyphs { - if i == finalGlyphIdx { - l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncatedCount - } else { - l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0 - } - } -} - -// Range describes the position and quantity of a range of text elements -// within a larger slice. The unit is usually runes of unicode data or -// glyphs of shaped font data. -type Range struct { - // Count describes the number of items represented by the Range. - Count int - // Offset describes the start position of the represented - // items within a larger list. - Offset int -} - -// glyph contains the metadata needed to render a glyph. -type glyph struct { - // id is this glyph's identifier within the font it was shaped with. - id GlyphID - // clusterIndex is the identifier for the text shaping cluster that - // this glyph is part of. - clusterIndex int - // glyphCount is the number of glyphs in the same cluster as this glyph. - glyphCount int - // runeCount is the quantity of runes in the source text that this glyph - // corresponds to. - runeCount int - // xAdvance and yAdvance describe the distance the dot moves when - // laying out the glyph on the X or Y axis. - xAdvance, yAdvance fixed.Int26_6 - // xOffset and yOffset describe offsets from the dot that should be - // applied when rendering the glyph. - xOffset, yOffset fixed.Int26_6 - // bounds describes the visual bounding box of the glyph relative to - // its dot. - bounds fixed.Rectangle26_6 -} - -type runLayout struct { - // VisualPosition describes the relative position of this run of text within - // its line. It should be a valid index into the containing line's VisualOrder - // slice. - VisualPosition int - // X is the visual offset of the dot for the first glyph in this run - // relative to the beginning of the line. - X fixed.Int26_6 - // Glyphs are the actual font characters for the text. They are ordered - // from left to right regardless of the text direction of the underlying - // text. - Glyphs []glyph - // Runes describes the position of the text data this layout represents - // within the containing text.Line. - Runes Range - // Advance is the sum of the advances of all clusters in the Layout. - Advance fixed.Int26_6 - // PPEM is the pixels-per-em scale used to shape this run. - PPEM fixed.Int26_6 - // Direction is the layout direction of the glyphs. - Direction system.TextDirection - // face is the font face that the ID of each Glyph in the Layout refers to. - face *font.Face - // truncator indicates that this run is a text truncator standing in for remaining - // text. - truncator bool -} - -// shaperImpl implements the shaping and line-wrapping of opentype fonts. -type shaperImpl struct { - // Fields for tracking fonts/faces. - fontMap *fontscan.FontMap - faces []*font.Face - faceToIndex map[*font.Font]int - faceMeta []giofont.Font - defaultFaces []string - logger interface { - Printf(format string, args ...any) - } - parser parser - - // Shaping and wrapping state. - shaper shaping.HarfbuzzShaper - wrapper shaping.LineWrapper - bidiParagraph bidi.Paragraph - - // Scratch buffers used to avoid re-allocating slices during routine internal - // shaping operations. - splitScratch1, splitScratch2 []shaping.Input - outScratchBuf []shaping.Output - scratchRunes []rune - - // bitmapGlyphCache caches extracted bitmap glyph images. - bitmapGlyphCache bitmapCache -} - -// debugLogger only logs messages if debug.Text is true. -type debugLogger struct { - *log.Logger -} - -func newDebugLogger() debugLogger { - return debugLogger{Logger: log.New(log.Writer(), "[text] ", log.Default().Flags())} -} - -func (d debugLogger) Printf(format string, args ...any) { - if debug.Text.Load() { - d.Logger.Printf(format, args...) - } -} - -func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl { - var shaper shaperImpl - shaper.logger = newDebugLogger() - shaper.fontMap = fontscan.NewFontMap(shaper.logger) - shaper.faceToIndex = make(map[*font.Font]int) - if systemFonts { - str, err := os.UserCacheDir() - if err != nil { - shaper.logger.Printf("failed resolving font cache dir: %v", err) - shaper.logger.Printf("skipping system font load") - } - if err := shaper.fontMap.UseSystemFonts(str); err != nil { - shaper.logger.Printf("failed loading system fonts: %v", err) - } - } - for _, f := range collection { - shaper.Load(f) - shaper.defaultFaces = append(shaper.defaultFaces, string(f.Font.Typeface)) - } - shaper.shaper.SetFontCacheSize(32) - return &shaper -} - -// Load registers the provided FontFace with the shaper, if it is compatible. -// It returns whether the face is now available for use. FontFaces are prioritized -// in the order in which they are loaded, with the first face being the default. -func (s *shaperImpl) Load(f FontFace) { - desc := opentype.FontToDescription(f.Font) - s.fontMap.AddFace(f.Face.Face(), fontscan.Location{File: fmt.Sprint(desc)}, desc) - s.addFace(f.Face.Face(), f.Font) -} - -func (s *shaperImpl) addFace(f *font.Face, md giofont.Font) { - if _, ok := s.faceToIndex[f.Font]; ok { - return - } - s.logger.Printf("loaded face %s(style:%s, weight:%d)", md.Typeface, md.Style, md.Weight) - idx := len(s.faces) - s.faceToIndex[f.Font] = idx - s.faces = append(s.faces, f) - s.faceMeta = append(s.faceMeta, md) -} - -// splitByScript divides the inputs into new, smaller inputs on script boundaries -// and correctly sets the text direction per-script. It will -// use buf as the backing memory for the returned slice if buf is non-nil. -func splitByScript(inputs []shaping.Input, documentDir di.Direction, buf []shaping.Input) []shaping.Input { - var splitInputs []shaping.Input - if buf == nil { - splitInputs = make([]shaping.Input, 0, len(inputs)) - } else { - splitInputs = buf - } - for _, input := range inputs { - currentInput := input - if input.RunStart == input.RunEnd { - return []shaping.Input{input} - } - firstNonCommonRune := input.RunStart - for i := firstNonCommonRune; i < input.RunEnd; i++ { - if language.LookupScript(input.Text[i]) != language.Common { - firstNonCommonRune = i - break - } - } - currentInput.Script = language.LookupScript(input.Text[firstNonCommonRune]) - for i := firstNonCommonRune + 1; i < input.RunEnd; i++ { - r := input.Text[i] - runeScript := language.LookupScript(r) - - if runeScript == language.Common || runeScript == currentInput.Script { - continue - } - - if i != input.RunStart { - currentInput.RunEnd = i - splitInputs = append(splitInputs, currentInput) - } - - currentInput = input - currentInput.RunStart = i - currentInput.Script = runeScript - // In the future, it may make sense to try to guess the language of the text here as well, - // but this is a complex process. - } - // close and add the last input - currentInput.RunEnd = input.RunEnd - splitInputs = append(splitInputs, currentInput) - } - - return splitInputs -} - -func (s *shaperImpl) splitBidi(input shaping.Input) []shaping.Input { - var splitInputs []shaping.Input - if input.Direction.Axis() != di.Horizontal || input.RunStart == input.RunEnd { - return []shaping.Input{input} - } - def := bidi.LeftToRight - if input.Direction.Progression() == di.TowardTopLeft { - def = bidi.RightToLeft - } - s.bidiParagraph.SetString(string(input.Text), bidi.DefaultDirection(def)) - out, err := s.bidiParagraph.Order() - if err != nil { - return []shaping.Input{input} - } - for i := range out.NumRuns() { - currentInput := input - run := out.Run(i) - dir := run.Direction() - _, endRune := run.Pos() - currentInput.RunEnd = endRune + 1 - if dir == bidi.RightToLeft { - currentInput.Direction = di.DirectionRTL - } else { - currentInput.Direction = di.DirectionLTR - } - splitInputs = append(splitInputs, currentInput) - input.RunStart = currentInput.RunEnd - } - return splitInputs -} - -// ResolveFace allows shaperImpl to implement shaping.FontMap, wrapping its fontMap -// field and ensuring that any faces loaded as part of the search are registered with -// ids so that they can be referred to by a GlyphID. -func (s *shaperImpl) ResolveFace(r rune) *font.Face { - face := s.fontMap.ResolveFace(r) - if face != nil { - family, aspect := s.fontMap.FontMetadata(face.Font) - md := opentype.DescriptionToFont(font.Description{ - Family: family, - Aspect: aspect, - }) - s.addFace(face, md) - return face - } - return nil -} - -// splitByFaces divides the inputs by font coverage in the provided faces. It will use the slice provided in buf -// as the backing storage of the returned slice if buf is non-nil. -func (s *shaperImpl) splitByFaces(inputs []shaping.Input, buf []shaping.Input) []shaping.Input { - var split []shaping.Input - if buf == nil { - split = make([]shaping.Input, 0, len(inputs)) - } else { - split = buf - } - for _, input := range inputs { - split = append(split, shaping.SplitByFace(input, s)...) - } - return split -} - -// shapeText invokes the text shaper and returns the raw text data in the shaper's native -// format. It does not wrap lines. -func (s *shaperImpl) shapeText(ppem fixed.Int26_6, lc system.Locale, txt []rune) []shaping.Output { - lcfg := langConfig{ - Language: language.NewLanguage(lc.Language), - Direction: mapDirection(lc.Direction), - } - // Create an initial input. - input := toInput(nil, ppem, lcfg, txt) - if input.RunStart == input.RunEnd && len(s.faces) > 0 { - // Give the empty string a face. This is a necessary special case because - // the face splitting process works by resolving faces for each rune, and - // the empty string contains no runes. - input.Face = s.faces[0] - } - // Break input on font glyph coverage. - inputs := s.splitBidi(input) - inputs = s.splitByFaces(inputs, s.splitScratch1[:0]) - inputs = splitByScript(inputs, lcfg.Direction, s.splitScratch2[:0]) - // Shape all inputs. - if needed := len(inputs) - len(s.outScratchBuf); needed > 0 { - s.outScratchBuf = slices.Grow(s.outScratchBuf, needed) - } - s.outScratchBuf = s.outScratchBuf[:0] - for _, input := range inputs { - if input.Face != nil { - s.outScratchBuf = append(s.outScratchBuf, s.shaper.Shape(input)) - } else { - s.outScratchBuf = append(s.outScratchBuf, shaping.Output{ - // Use the text size as the advance of the entire fake run so that - // it doesn't occupy zero space. - Advance: input.Size, - Size: input.Size, - Glyphs: []shaping.Glyph{ - { - Width: input.Size, - Height: input.Size, - XBearing: 0, - YBearing: 0, - XAdvance: input.Size, - YAdvance: input.Size, - XOffset: 0, - YOffset: 0, - ClusterIndex: input.RunStart, - RuneCount: input.RunEnd - input.RunStart, - GlyphCount: 1, - GlyphID: 0, - Mask: 0, - }, - }, - LineBounds: shaping.Bounds{ - Ascent: input.Size, - Descent: 0, - Gap: 0, - }, - GlyphBounds: shaping.Bounds{ - Ascent: input.Size, - Descent: 0, - Gap: 0, - }, - Direction: input.Direction, - Runes: shaping.Range{ - Offset: input.RunStart, - Count: input.RunEnd - input.RunStart, - }, - }) - } - } - return s.outScratchBuf -} - -func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy { - switch p { - case WrapGraphemes: - return shaping.Always - case WrapWords: - return shaping.Never - default: - return shaping.WhenNecessary - } -} - -// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format. -func (s *shaperImpl) shapeAndWrapText(params Parameters, txt []rune) (_ []shaping.Line, truncated int) { - wc := shaping.WrapConfig{ - Direction: mapDirection(params.Locale.Direction), - TruncateAfterLines: params.MaxLines, - TextContinues: params.forceTruncate, - BreakPolicy: wrapPolicyToGoText(params.WrapPolicy), - DisableTrailingWhitespaceTrim: params.DisableSpaceTrim, - } - families := s.defaultFaces - if params.Font.Typeface != "" { - parsed, err := s.parser.parse(string(params.Font.Typeface)) - if err != nil { - s.logger.Printf("Unable to parse typeface %q: %v", params.Font.Typeface, err) - } else { - families = parsed - } - } - s.fontMap.SetQuery(fontscan.Query{ - Families: families, - Aspect: opentype.FontToDescription(params.Font).Aspect, - }) - if wc.TruncateAfterLines > 0 { - if len(params.Truncator) == 0 { - params.Truncator = "…" - } - // We only permit a single run as the truncator, regardless of whether more were generated. - // Just use the first one. - wc.Truncator = s.shapeText(params.PxPerEm, params.Locale, []rune(params.Truncator))[0] - } - // Wrap outputs into lines. - return s.wrapper.WrapParagraph(wc, params.MaxWidth, txt, shaping.NewSliceIterator(s.shapeText(params.PxPerEm, params.Locale, txt))) -} - -// replaceControlCharacters replaces problematic unicode -// code points with spaces to ensure proper rune accounting. -func replaceControlCharacters(in []rune) []rune { - for i, r := range in { - switch r { - // ASCII File separator. - case '\u001C': - // ASCII Group separator. - case '\u001D': - // ASCII Record separator. - case '\u001E': - case '\r': - case '\n': - // Unicode "next line" character. - case '\u0085': - // Unicode "paragraph separator". - case '\u2029': - default: - continue - } - in[i] = ' ' - } - return in -} - -// Layout shapes and wraps the text, and returns the result in Gio's shaped text format. -func (s *shaperImpl) LayoutString(params Parameters, txt string) document { - return s.LayoutRunes(params, []rune(txt)) -} - -// Layout shapes and wraps the text, and returns the result in Gio's shaped text format. -func (s *shaperImpl) Layout(params Parameters, txt io.RuneReader) document { - s.scratchRunes = s.scratchRunes[:0] - for r, _, err := txt.ReadRune(); err != nil; r, _, err = txt.ReadRune() { - s.scratchRunes = append(s.scratchRunes, r) - } - return s.LayoutRunes(params, s.scratchRunes) -} - -func calculateYOffsets(lines []line) { - if len(lines) < 1 { - return - } - // Ceil the first value to ensure that we don't baseline it too close to the top of the - // viewport and cut off the top pixel. - currentY := lines[0].ascent.Ceil() - for i := range lines { - if i > 0 { - currentY += lines[i].lineHeight.Round() - } - lines[i].yOffset = currentY - } -} - -// LayoutRunes shapes and wraps the text, and returns the result in Gio's shaped text format. -func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document { - hasNewline := len(txt) > 0 && txt[len(txt)-1] == '\n' - var ls []shaping.Line - var truncated int - if hasNewline { - txt = txt[:len(txt)-1] - } - if params.MaxLines != 0 && hasNewline { - // If we might end up truncating a trailing newline, we must insert the truncator symbol - // on the final line (if we hit the limit). - params.forceTruncate = true - } - ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt)) - - hasTruncator := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls)) - if hasTruncator && hasNewline { - // We have a truncator at the end of the line, so the newline is logically - // truncated as well. - truncated++ - hasNewline = false - } - - // Convert to Lines. - textLines := make([]line, len(ls)) - maxHeight := fixed.Int26_6(0) - for i := range ls { - otLine := toLine(s.faceToIndex, ls[i], params.Locale.Direction) - if otLine.lineHeight > maxHeight { - maxHeight = otLine.lineHeight - } - if isFinalLine := i == len(ls)-1; isFinalLine { - if hasNewline { - otLine.insertTrailingSyntheticNewline(len(txt)) - } - if hasTruncator { - otLine.setTruncatedCount(truncated) - } - } - textLines[i] = otLine - } - if params.LineHeight != 0 { - maxHeight = params.LineHeight - } - if params.LineHeightScale == 0 { - params.LineHeightScale = 1.2 - } - - maxHeight = floatToFixed(fixedToFloat(maxHeight) * params.LineHeightScale) - for i := range textLines { - textLines[i].lineHeight = maxHeight - } - calculateYOffsets(textLines) - return document{ - lines: textLines, - alignment: params.Alignment, - alignWidth: alignWidth(params.MinWidth, textLines), - } -} - -func alignWidth(minWidth int, lines []line) int { - for _, l := range lines { - minWidth = max(minWidth, l.width.Ceil()) - } - return minWidth -} - -// Shape converts the provided glyphs into a path. The path will enclose the forms -// of all vector glyphs. -func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec { - var lastPos f32.Point - var x fixed.Int26_6 - var builder clip.Path - builder.Begin(pathOps) - for i, g := range gs { - if i == 0 { - x = g.X - } - ppem, faceIdx, gid := splitGlyphID(g.ID) - if faceIdx >= len(s.faces) { - continue - } - face := s.faces[faceIdx] - if face == nil { - continue - } - scaleFactor := fixedToFloat(ppem) / float32(face.Upem()) - glyphData := face.GlyphData(gid) - switch glyphData := glyphData.(type) { - case font.GlyphOutline: - outline := glyphData - // Move to glyph position. - pos := f32.Point{ - X: fixedToFloat((g.X - x) - g.Offset.X), - Y: -fixedToFloat(g.Offset.Y), - } - builder.Move(pos.Sub(lastPos)) - lastPos = pos - var lastArg f32.Point - - // Convert fonts.Segments to relative segments. - for _, fseg := range outline.Segments { - nargs := 1 - switch fseg.Op { - case gotextot.SegmentOpQuadTo: - nargs = 2 - case gotextot.SegmentOpCubeTo: - nargs = 3 - } - var args [3]f32.Point - for i := range nargs { - a := f32.Point{ - X: fseg.Args[i].X * scaleFactor, - Y: -fseg.Args[i].Y * scaleFactor, - } - args[i] = a.Sub(lastArg) - if i == nargs-1 { - lastArg = a - } - } - switch fseg.Op { - case gotextot.SegmentOpMoveTo: - builder.Move(args[0]) - case gotextot.SegmentOpLineTo: - builder.Line(args[0]) - case gotextot.SegmentOpQuadTo: - builder.Quad(args[0], args[1]) - case gotextot.SegmentOpCubeTo: - builder.Cube(args[0], args[1], args[2]) - default: - panic("unsupported segment op") - } - } - lastPos = lastPos.Add(lastArg) - } - } - return builder.End() -} - -func fixedToFloat(i fixed.Int26_6) float32 { - return float32(i) / 64.0 -} - -func floatToFixed(f float32) fixed.Int26_6 { - return fixed.Int26_6(f * 64) -} - -// Bitmaps returns an op.CallOp that will display all bitmap glyphs within gs. -// The positioning of the bitmaps uses the same logic as Shape(), so the returned -// CallOp can be added at the same offset as the path data returned by Shape() -// and will align correctly. -func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp { - var x fixed.Int26_6 - bitmapMacro := op.Record(ops) - for i, g := range gs { - if i == 0 { - x = g.X - } - _, faceIdx, gid := splitGlyphID(g.ID) - if faceIdx >= len(s.faces) { - continue - } - face := s.faces[faceIdx] - if face == nil { - continue - } - glyphData := face.GlyphData(gid) - switch glyphData := glyphData.(type) { - case font.GlyphBitmap: - var imgOp paint.ImageOp - var imgSize image.Point - bitmapData, ok := s.bitmapGlyphCache.Get(g.ID) - if !ok { - var img image.Image - switch glyphData.Format { - case font.PNG, font.JPG, font.TIFF: - img, _, _ = image.Decode(bytes.NewReader(glyphData.Data)) - case font.BlackAndWhite: - // This is a complex family of uncompressed bitmaps that don't seem to be - // very common in practice. We can try adding support later if needed. - fallthrough - default: - // Unknown format. - continue - } - imgOp = paint.NewImageOp(img) - imgSize = img.Bounds().Size() - s.bitmapGlyphCache.Put(g.ID, bitmap{img: imgOp, size: imgSize}) - } else { - imgOp = bitmapData.img - imgSize = bitmapData.size - } - off := op.Affine(f32.AffineId().Offset(f32.Point{ - X: fixedToFloat((g.X - x) - g.Offset.X), - Y: fixedToFloat(g.Offset.Y + g.Bounds.Min.Y), - })).Push(ops) - cl := clip.Rect{Max: imgSize}.Push(ops) - - glyphSize := image.Rectangle{ - Min: image.Point{ - X: g.Bounds.Min.X.Round(), - Y: g.Bounds.Min.Y.Round(), - }, - Max: image.Point{ - X: g.Bounds.Max.X.Round(), - Y: g.Bounds.Max.Y.Round(), - }, - }.Size() - aff := op.Affine(f32.AffineId().Scale(f32.Point{}, f32.Point{ - X: float32(glyphSize.X) / float32(imgSize.X), - Y: float32(glyphSize.Y) / float32(imgSize.Y), - })).Push(ops) - imgOp.Add(ops) - paint.PaintOp{}.Add(ops) - aff.Pop() - cl.Pop() - off.Pop() - } - } - return bitmapMacro.Stop() -} - -// langConfig describes the language and writing system of a body of text. -type langConfig struct { - // Language the text is written in. - language.Language - // Writing system used to represent the text. - language.Script - // Direction of the text, usually driven by the writing system. - di.Direction -} - -// toInput converts its parameters into a shaping.Input. -func toInput(face *font.Face, ppem fixed.Int26_6, lc langConfig, runes []rune) shaping.Input { - var input shaping.Input - input.Direction = lc.Direction - input.Text = runes - input.Size = ppem - input.Face = face - input.Language = lc.Language - input.Script = lc.Script - input.RunStart = 0 - input.RunEnd = len(runes) - return input -} - -func mapDirection(d system.TextDirection) di.Direction { - switch d { - case system.LTR: - return di.DirectionLTR - case system.RTL: - return di.DirectionRTL - } - return di.DirectionLTR -} - -func unmapDirection(d di.Direction) system.TextDirection { - switch d { - case di.DirectionLTR: - return system.LTR - case di.DirectionRTL: - return system.RTL - } - return system.LTR -} - -// toGioGlyphs converts text shaper glyphs into the minimal representation -// that Gio needs. -func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph { - out := make([]glyph, 0, len(in)) - for _, g := range in { - // To better understand how to calculate the bounding box, see here: - // https://freetype.org/freetype2/docs/glyphs/glyph-metrics-3.svg - var bounds fixed.Rectangle26_6 - bounds.Min.X = g.XBearing - bounds.Min.Y = -g.YBearing - bounds.Max = bounds.Min.Add(fixed.Point26_6{X: g.Width, Y: -g.Height}) - out = append(out, glyph{ - id: newGlyphID(ppem, faceIdx, g.GlyphID), - clusterIndex: g.ClusterIndex, - runeCount: g.RuneCount, - glyphCount: g.GlyphCount, - xAdvance: g.XAdvance, - yAdvance: g.YAdvance, - xOffset: g.XOffset, - yOffset: g.YOffset, - bounds: bounds, - }) - } - return out -} - -// toLine converts the output into a Line with the provided dominant text direction. -func toLine(faceToIndex map[*font.Font]int, o shaping.Line, dir system.TextDirection) line { - if len(o) < 1 { - return line{} - } - line := line{ - runs: make([]runLayout, len(o)), - direction: dir, - visualOrder: make([]int, len(o)), - } - maxSize := fixed.Int26_6(0) - for i := range o { - run := o[i] - if run.Size > maxSize { - maxSize = run.Size - } - var font *font.Font - if run.Face != nil { - font = run.Face.Font - } - line.runs[i] = runLayout{ - Glyphs: toGioGlyphs(run.Glyphs, run.Size, faceToIndex[font]), - Runes: Range{ - Count: run.Runes.Count, - Offset: line.runeCount, - }, - Direction: unmapDirection(run.Direction), - face: run.Face, - Advance: run.Advance, - PPEM: run.Size, - VisualPosition: int(run.VisualIndex), - } - line.visualOrder[run.VisualIndex] = i - line.runeCount += run.Runes.Count - line.width += run.Advance - if line.ascent < run.LineBounds.Ascent { - line.ascent = run.LineBounds.Ascent - } - if line.descent < -run.LineBounds.Descent+run.LineBounds.Gap { - line.descent = -run.LineBounds.Descent + run.LineBounds.Gap - } - } - line.lineHeight = maxSize - // Iterate and resolve the X of each run. - x := fixed.Int26_6(0) - for _, runIdx := range line.visualOrder { - line.runs[runIdx].X = x - x += line.runs[runIdx].Advance - } - return line -} diff --git a/gio/text/gotext_test.go b/gio/text/gotext_test.go deleted file mode 100644 index d12e386..0000000 --- a/gio/text/gotext_test.go +++ /dev/null @@ -1,595 +0,0 @@ -package text - -import ( - "fmt" - "math" - "slices" - "strconv" - "testing" - - nsareg "eliasnaur.com/font/noto/sans/arabic/regular" - "github.com/go-text/typesetting/font" - "github.com/go-text/typesetting/shaping" - "golang.org/x/image/font/gofont/goregular" - "golang.org/x/image/math/fixed" - - giofont "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/font/opentype" - "github.com/p9c/p9/pkg/gel/gio/io/system" -) - -var english = system.Locale{ - Language: "EN", - Direction: system.LTR, -} - -var arabic = system.Locale{ - Language: "AR", - Direction: system.RTL, -} - -func testShaper(faces ...giofont.Face) *shaperImpl { - ff := make([]FontFace, 0, len(faces)) - for _, face := range faces { - ff = append(ff, FontFace{Face: face}) - } - shaper := newShaperImpl(false, ff) - return shaper -} - -func TestEmptyString(t *testing.T) { - ppem := fixed.I(200) - ltrFace, _ := opentype.Parse(goregular.TTF) - shaper := testShaper(ltrFace) - - lines := shaper.LayoutRunes(Parameters{ - PxPerEm: ppem, - MaxWidth: 2000, - Locale: english, - }, []rune{}) - if len(lines.lines) == 0 { - t.Fatalf("Layout returned no lines for empty string; expected 1") - } - l := lines.lines[0] - if expected := fixed.Int26_6(12094); l.ascent != expected { - t.Errorf("unexpected ascent for empty string: %v, expected %v", l.ascent, expected) - } - if expected := fixed.Int26_6(2700); l.descent != expected { - t.Errorf("unexpected descent for empty string: %v, expected %v", l.descent, expected) - } -} - -func TestNoFaces(t *testing.T) { - ppem := fixed.I(200) - shaper := testShaper() - - // Ensure shaping text with no faces does not panic. - shaper.LayoutRunes(Parameters{ - PxPerEm: ppem, - MaxWidth: 2000, - Locale: english, - }, []rune("✨ⷽℎ↞⋇ⱜ⪫⢡⽛⣦␆Ⱨⳏ⳯⒛⭣╎⌞⟻⢇┃➡⬎⩱⸇ⷎ⟅▤⼶⇺⩳⎏⤬⬞ⴈ⋠⿶⢒₍☟⽂ⶦ⫰⭢⌹∼▀⾯⧂❽⩏ⓖ⟅⤔⍇␋⽓ₑ⢳⠑❂⊪⢘⽨⃯▴ⷿ")) -} - -func TestAlignWidth(t *testing.T) { - lines := []line{ - {width: fixed.I(50)}, - {width: fixed.I(75)}, - {width: fixed.I(25)}, - } - for _, minWidth := range []int{0, 50, 100} { - width := alignWidth(minWidth, lines) - if width < minWidth { - t.Errorf("expected width >= %d, got %d", minWidth, width) - } - } -} - -func TestShapingAlignWidth(t *testing.T) { - ppem := fixed.I(10) - ltrFace, _ := opentype.Parse(goregular.TTF) - shaper := testShaper(ltrFace) - - type testcase struct { - name string - minWidth, maxWidth int - expected int - str string - } - for _, tc := range []testcase{ - { - name: "zero min", - maxWidth: 100, - str: "a\nb\nc", - expected: 22, - }, - { - name: "min == max", - minWidth: 100, - maxWidth: 100, - str: "a\nb\nc", - expected: 100, - }, - { - name: "min < max", - minWidth: 50, - maxWidth: 100, - str: "a\nb\nc", - expected: 50, - }, - { - name: "min < max, text > min", - minWidth: 50, - maxWidth: 100, - str: "aphabetic\nb\nc", - expected: 60, - }, - } { - t.Run(tc.name, func(t *testing.T) { - lines := shaper.LayoutString(Parameters{ - PxPerEm: ppem, - MinWidth: tc.minWidth, - MaxWidth: tc.maxWidth, - Locale: english, - }, tc.str) - if lines.alignWidth != tc.expected { - t.Errorf("expected line alignWidth to be %d, got %d", tc.expected, lines.alignWidth) - } - }) - } -} - -// TestNewlineSynthesis ensures that the shaper correctly inserts synthetic glyphs -// representing newline runes. -func TestNewlineSynthesis(t *testing.T) { - ppem := fixed.I(10) - ltrFace, _ := opentype.Parse(goregular.TTF) - rtlFace, _ := opentype.Parse(nsareg.TTF) - shaper := testShaper(ltrFace, rtlFace) - - type testcase struct { - name string - locale system.Locale - txt string - } - for _, tc := range []testcase{ - { - name: "ltr bidi newline in rtl segment", - locale: english, - txt: "The quick سماء שלום لا fox تمط שלום\n", - }, - { - name: "ltr bidi newline in ltr segment", - locale: english, - txt: "The quick سماء שלום لا fox\n", - }, - { - name: "rtl bidi newline in ltr segment", - locale: arabic, - txt: "الحب سماء brown привет fox تمط jumps\n", - }, - { - name: "rtl bidi newline in rtl segment", - locale: arabic, - txt: "الحب سماء brown привет fox تمط\n", - }, - } { - t.Run(tc.name, func(t *testing.T) { - doc := shaper.LayoutRunes(Parameters{ - PxPerEm: ppem, - MaxWidth: 200, - Locale: tc.locale, - }, []rune(tc.txt)) - for lineIdx, line := range doc.lines { - lastRunIdx := len(line.runs) - 1 - lastRun := line.runs[lastRunIdx] - lastGlyphIdx := len(lastRun.Glyphs) - 1 - if lastRun.Direction.Progression() == system.TowardOrigin { - lastGlyphIdx = 0 - } - glyph := lastRun.Glyphs[lastGlyphIdx] - if glyph.glyphCount != 0 { - t.Errorf("expected synthetic newline on line %d, run %d, glyph %d", lineIdx, lastRunIdx, lastGlyphIdx) - } - for runIdx, run := range line.runs { - for glyphIdx, glyph := range run.Glyphs { - if runIdx == lastRunIdx && glyphIdx == lastGlyphIdx { - continue - } - if glyph.glyphCount == 0 { - t.Errorf("found invalid synthetic newline on line %d, run %d, glyph %d", lineIdx, runIdx, glyphIdx) - } - } - } - } - if t.Failed() { - printLinePositioning(t, doc.lines, nil) - } - }) - } -} - -// simpleGlyph returns a simple square glyph with the provided cluster -// value. -func simpleGlyph(cluster int) shaping.Glyph { - return complexGlyph(cluster, 1, 1) -} - -// ligatureGlyph returns a simple square glyph with the provided cluster -// value and number of runes. -func ligatureGlyph(cluster, runes int) shaping.Glyph { - return complexGlyph(cluster, runes, 1) -} - -// expansionGlyph returns a simple square glyph with the provided cluster -// value and number of glyphs. -func expansionGlyph(cluster, glyphs int) shaping.Glyph { - return complexGlyph(cluster, 1, glyphs) -} - -// complexGlyph returns a simple square glyph with the provided cluster -// value, number of associated runes, and number of glyphs in the cluster. -func complexGlyph(cluster, runes, glyphs int) shaping.Glyph { - return shaping.Glyph{ - Width: fixed.I(10), - Height: fixed.I(10), - XAdvance: fixed.I(10), - YAdvance: fixed.I(10), - YBearing: fixed.I(10), - ClusterIndex: cluster, - GlyphCount: glyphs, - RuneCount: runes, - } -} - -// copyLines performs a deep copy of the provided lines. This is necessary if you -// want to use the line wrapper again while also using the lines. -func copyLines(lines []shaping.Line) []shaping.Line { - out := make([]shaping.Line, len(lines)) - for lineIdx, line := range lines { - lineCopy := make([]shaping.Output, len(line)) - for runIdx, run := range line { - lineCopy[runIdx] = run - lineCopy[runIdx].Glyphs = slices.Clone(run.Glyphs) - } - out[lineIdx] = lineCopy - } - return out -} - -// makeTestText creates a simple and complex(bidi) sample of shaped text at the given -// font size and wrapped to the given line width. The runeLimit, if nonzero, -// truncates the sample text to ensure shorter output for expensive tests. -func makeTestText(shaper *shaperImpl, primaryDir system.TextDirection, fontSize, lineWidth, runeLimit int) (simpleSample, complexSample []shaping.Line) { - ltrFace, _ := opentype.Parse(goregular.TTF) - rtlFace, _ := opentype.Parse(nsareg.TTF) - if shaper == nil { - shaper = testShaper(ltrFace, rtlFace) - } - - ltrSource := "The quick brown fox jumps over the lazy dog." - rtlSource := "الحب سماء لا تمط غير الأحلام" - // bidiSource is crafted to contain multiple consecutive RTL runs (by - // changing scripts within the RTL). - bidiSource := "The quick سماء שלום لا fox تمط שלום غير the lazy dog." - // bidi2Source is crafted to contain multiple consecutive LTR runs (by - // changing scripts within the LTR). - bidi2Source := "الحب سماء brown привет fox تمط jumps привет over غير الأحلام" - - locale := english - simpleSource := ltrSource - complexSource := bidiSource - if primaryDir == system.RTL { - simpleSource = rtlSource - complexSource = bidi2Source - locale = arabic - } - if runeLimit != 0 { - simpleRunes := []rune(simpleSource) - complexRunes := []rune(complexSource) - if runeLimit < len(simpleRunes) { - ltrSource = string(simpleRunes[:runeLimit]) - } - if runeLimit < len(complexRunes) { - rtlSource = string(complexRunes[:runeLimit]) - } - } - simpleText, _ := shaper.shapeAndWrapText(Parameters{ - PxPerEm: fixed.I(fontSize), - MaxWidth: lineWidth, - Locale: locale, - }, []rune(simpleSource)) - simpleText = copyLines(simpleText) - complexText, _ := shaper.shapeAndWrapText(Parameters{ - PxPerEm: fixed.I(fontSize), - MaxWidth: lineWidth, - Locale: locale, - }, []rune(complexSource)) - complexText = copyLines(complexText) - testShaper(rtlFace, ltrFace) - return simpleText, complexText -} - -func fixedAbs(a fixed.Int26_6) fixed.Int26_6 { - if a < 0 { - a = -a - } - return a -} - -func TestToLine(t *testing.T) { - ltrFace, _ := opentype.Parse(goregular.TTF) - rtlFace, _ := opentype.Parse(nsareg.TTF) - shaper := testShaper(ltrFace, rtlFace) - ltr, bidi := makeTestText(shaper, system.LTR, 16, 100, 0) - rtl, bidi2 := makeTestText(shaper, system.RTL, 16, 100, 0) - _, bidiWide := makeTestText(shaper, system.LTR, 16, 200, 0) - _, bidi2Wide := makeTestText(shaper, system.RTL, 16, 200, 0) - type testcase struct { - name string - lines []shaping.Line - // Dominant text direction. - dir system.TextDirection - } - for _, tc := range []testcase{ - { - name: "ltr", - lines: ltr, - dir: system.LTR, - }, - { - name: "rtl", - lines: rtl, - dir: system.RTL, - }, - { - name: "bidi", - lines: bidi, - dir: system.LTR, - }, - { - name: "bidi2", - lines: bidi2, - dir: system.RTL, - }, - { - name: "bidi_wide", - lines: bidiWide, - dir: system.LTR, - }, - { - name: "bidi2_wide", - lines: bidi2Wide, - dir: system.RTL, - }, - } { - t.Run(tc.name, func(t *testing.T) { - // We expect: - // - Line dimensions to be populated. - // - Line direction to be populated. - // - Runs to be ordered from lowest runes first. - // - Runs to have widths matching the input. - // - Runs to have the same total number of glyphs/runes as the input. - runesSeen := Range{} - shaper := testShaper(ltrFace, rtlFace) - for i, input := range tc.lines { - seenRun := make([]bool, len(input)) - inputLowestRuneOffset := math.MaxInt - totalInputGlyphs := 0 - totalInputRunes := 0 - for _, run := range input { - if run.Runes.Offset < inputLowestRuneOffset { - inputLowestRuneOffset = run.Runes.Offset - } - totalInputGlyphs += len(run.Glyphs) - totalInputRunes += run.Runes.Count - } - output := toLine(shaper.faceToIndex, input, tc.dir) - if output.direction != tc.dir { - t.Errorf("line %d: expected direction %v, got %v", i, tc.dir, output.direction) - } - totalRunWidth := fixed.I(0) - totalLineGlyphs := 0 - totalLineRunes := 0 - for k, run := range output.runs { - seenRun[run.VisualPosition] = true - if output.visualOrder[run.VisualPosition] != k { - t.Errorf("line %d, run %d: run.VisualPosition=%d, but line.VisualOrder[%d]=%d(should be %d)", i, k, run.VisualPosition, run.VisualPosition, output.visualOrder[run.VisualPosition], k) - } - if run.Runes.Offset != totalLineRunes { - t.Errorf("line %d, run %d: expected Runes.Offset to be %d, got %d", i, k, totalLineRunes, run.Runes.Offset) - } - runGlyphCount := len(run.Glyphs) - if inputGlyphs := len(input[k].Glyphs); runGlyphCount != inputGlyphs { - t.Errorf("line %d, run %d: expected %d glyphs, found %d", i, k, inputGlyphs, runGlyphCount) - } - runRuneCount := 0 - currentCluster := -1 - for _, g := range run.Glyphs { - if g.clusterIndex != currentCluster { - runRuneCount += g.runeCount - currentCluster = g.clusterIndex - } - } - if run.Runes.Count != runRuneCount { - t.Errorf("line %d, run %d: expected %d runes, counted %d", i, k, run.Runes.Count, runRuneCount) - } - runesSeen.Count += run.Runes.Count - totalRunWidth += fixedAbs(run.Advance) - totalLineGlyphs += len(run.Glyphs) - totalLineRunes += run.Runes.Count - } - if output.runeCount != totalInputRunes { - t.Errorf("line %d: input had %d runes, only counted %d", i, totalInputRunes, output.runeCount) - } - if totalLineGlyphs != totalInputGlyphs { - t.Errorf("line %d: input had %d glyphs, only counted %d", i, totalInputRunes, totalLineGlyphs) - } - if totalRunWidth != output.width { - t.Errorf("line %d: expected width %d, got %d", i, totalRunWidth, output.width) - } - for runIndex, seen := range seenRun { - if !seen { - t.Errorf("line %d, run %d missing from runs VisualPosition fields", i, runIndex) - } - } - } - lastLine := tc.lines[len(tc.lines)-1] - maxRunes := 0 - for _, run := range lastLine { - if run.Runes.Count+run.Runes.Offset > maxRunes { - maxRunes = run.Runes.Count + run.Runes.Offset - } - } - if runesSeen.Count != maxRunes { - t.Errorf("input covered %d runes, output only covers %d", maxRunes, runesSeen.Count) - } - }) - } -} - -func FuzzLayout(f *testing.F) { - ltrFace, _ := opentype.Parse(goregular.TTF) - rtlFace, _ := opentype.Parse(nsareg.TTF) - f.Add("د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.", true, false, uint8(10), uint16(200)) - - shaper := testShaper(ltrFace, rtlFace) - f.Fuzz(func(t *testing.T, txt string, rtl bool, truncate bool, fontSize uint8, width uint16) { - locale := system.Locale{ - Direction: system.LTR, - } - if rtl { - locale.Direction = system.RTL - } - if fontSize < 1 { - fontSize = 1 - } - maxLines := 0 - if truncate { - maxLines = 1 - } - lines := shaper.LayoutRunes(Parameters{ - PxPerEm: fixed.I(int(fontSize)), - MaxWidth: int(width), - MaxLines: maxLines, - Locale: locale, - }, []rune(txt)) - validateLines(t, lines.lines, len([]rune(txt))) - }) -} - -func validateLines(t *testing.T, lines []line, expectedRuneCount int) { - t.Helper() - runesSeen := 0 - for i, line := range lines { - totalRunWidth := fixed.I(0) - totalLineGlyphs := 0 - lineRunesSeen := 0 - for k, run := range line.runs { - if line.visualOrder[run.VisualPosition] != k { - t.Errorf("line %d, run %d: run.VisualPosition=%d, but line.VisualOrder[%d]=%d(should be %d)", i, k, run.VisualPosition, run.VisualPosition, line.visualOrder[run.VisualPosition], k) - } - if run.Runes.Offset != lineRunesSeen { - t.Errorf("line %d, run %d: expected Runes.Offset to be %d, got %d", i, k, lineRunesSeen, run.Runes.Offset) - } - runRuneCount := 0 - currentCluster := -1 - for _, g := range run.Glyphs { - if g.clusterIndex != currentCluster { - runRuneCount += g.runeCount - currentCluster = g.clusterIndex - } - } - if run.Runes.Count != runRuneCount { - t.Errorf("line %d, run %d: expected %d runes, counted %d", i, k, run.Runes.Count, runRuneCount) - } - lineRunesSeen += run.Runes.Count - totalRunWidth += fixedAbs(run.Advance) - totalLineGlyphs += len(run.Glyphs) - } - if totalRunWidth != line.width { - t.Errorf("line %d: expected width %d, got %d", i, line.width, totalRunWidth) - } - runesSeen += lineRunesSeen - } - if runesSeen != expectedRuneCount { - t.Errorf("input covered %d runes, output only covers %d", expectedRuneCount, runesSeen) - } -} - -// TestTextAppend ensures that appending two texts together correctly updates the new lines' -// y offsets. -func TestTextAppend(t *testing.T) { - ltrFace, _ := opentype.Parse(goregular.TTF) - rtlFace, _ := opentype.Parse(nsareg.TTF) - - shaper := testShaper(ltrFace, rtlFace) - - text1 := shaper.LayoutString(Parameters{ - PxPerEm: fixed.I(14), - MaxWidth: 200, - Locale: english, - }, "د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.") - text2 := shaper.LayoutString(Parameters{ - PxPerEm: fixed.I(14), - MaxWidth: 200, - Locale: english, - }, "د عرمثال dstي met لم aqل جدmوpمg lرe dرd لو عل ميrةsdiduntut lab renنيتذدagلaaiua.ئPocttأior رادرsاي mيrbلmnonaيdتد ماةعcلخ.") - - text1.append(text2) - curY := math.MinInt - for lineNum, line := range text1.lines { - yOff := line.yOffset - if yOff <= curY { - t.Errorf("lines[%d] has y offset %d, <= to previous %d", lineNum, yOff, curY) - } - curY = yOff - } -} - -func TestGlyphIDPacking(t *testing.T) { - const maxPPem = fixed.Int26_6((1 << sizebits) - 1) - type testcase struct { - name string - ppem fixed.Int26_6 - faceIndex int - gid font.GID - expected GlyphID - } - for _, tc := range []testcase{ - { - name: "zero value", - }, - { - name: "10 ppem faceIdx 1 GID 5", - ppem: fixed.I(10), - faceIndex: 1, - gid: 5, - expected: 284223755780101, - }, - { - name: maxPPem.String() + " ppem faceIdx " + strconv.Itoa(math.MaxUint16) + " GID " + fmt.Sprintf("%d", int64(math.MaxUint32)), - ppem: maxPPem, - faceIndex: math.MaxUint16, - gid: math.MaxUint32, - expected: 18446744073709551615, - }, - } { - t.Run(tc.name, func(t *testing.T) { - actual := newGlyphID(tc.ppem, tc.faceIndex, tc.gid) - if actual != tc.expected { - t.Errorf("expected %d, got %d", tc.expected, actual) - } - actualPPEM, actualFaceIdx, actualGID := splitGlyphID(actual) - if actualPPEM != tc.ppem { - t.Errorf("expected ppem %d, got %d", tc.ppem, actualPPEM) - } - if actualFaceIdx != tc.faceIndex { - t.Errorf("expected faceIdx %d, got %d", tc.faceIndex, actualFaceIdx) - } - if actualGID != tc.gid { - t.Errorf("expected gid %d, got %d", tc.gid, actualGID) - } - }) - } -} diff --git a/gio/text/lru.go b/gio/text/lru.go deleted file mode 100644 index 7306d20..0000000 --- a/gio/text/lru.go +++ /dev/null @@ -1,183 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package text - -import ( - "image" - "sync/atomic" - - giofont "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "golang.org/x/image/math/fixed" -) - -// entry holds a single key-value pair for an LRU cache. -type entry[K comparable, V any] struct { - next, prev *entry[K, V] - key K - v V -} - -// lru is a generic least-recently-used cache. -type lru[K comparable, V any] struct { - m map[K]*entry[K, V] - head, tail *entry[K, V] -} - -// Get fetches the value associated with the given key, if any. -func (l *lru[K, V]) Get(k K) (V, bool) { - if lt, ok := l.m[k]; ok { - l.remove(lt) - l.insert(lt) - return lt.v, true - } - var v V - return v, false -} - -// Put inserts the given value with the given key, evicting old -// cache entries if necessary. -func (l *lru[K, V]) Put(k K, v V) { - if l.m == nil { - l.m = make(map[K]*entry[K, V]) - l.head = new(entry[K, V]) - l.tail = new(entry[K, V]) - l.head.prev = l.tail - l.tail.next = l.head - } - val := &entry[K, V]{key: k, v: v} - l.m[k] = val - l.insert(val) - if len(l.m) > maxSize { - oldest := l.tail.next - l.remove(oldest) - delete(l.m, oldest.key) - } -} - -// remove cuts e out of the lru linked list. -func (l *lru[K, V]) remove(e *entry[K, V]) { - e.next.prev = e.prev - e.prev.next = e.next -} - -// insert adds e to the lru linked list. -func (l *lru[K, V]) insert(e *entry[K, V]) { - e.next = l.head - e.prev = l.head.prev - e.prev.next = e - e.next.prev = e -} - -type bitmapCache = lru[GlyphID, bitmap] - -type bitmap struct { - img paint.ImageOp - size image.Point -} - -type layoutCache = lru[layoutKey, document] - -type glyphValue[V any] struct { - v V - glyphs []glyphInfo -} - -type glyphLRU[V any] struct { - seed uint64 - cache lru[uint64, glyphValue[V]] -} - -var seed uint32 - -// hashGlyphs computes a hash key based on the ID and X offset of -// every glyph in the slice. -func (c *glyphLRU[V]) hashGlyphs(gs []Glyph) uint64 { - if c.seed == 0 { - c.seed = uint64(atomic.AddUint32(&seed, 3900798947)) - } - if len(gs) == 0 { - return 0 - } - - h := c.seed - firstX := gs[0].X - for _, g := range gs { - h += uint64(g.X - firstX) - h *= 6585573582091643 - h += uint64(g.ID) - h *= 3650802748644053 - } - - return h -} - -func (c *glyphLRU[V]) Get(key uint64, gs []Glyph) (V, bool) { - if v, ok := c.cache.Get(key); ok && gidsEqual(v.glyphs, gs) { - return v.v, true - } - var v V - return v, false -} - -func (c *glyphLRU[V]) Put(key uint64, glyphs []Glyph, v V) { - gids := make([]glyphInfo, len(glyphs)) - firstX := fixed.I(0) - for i, glyph := range glyphs { - if i == 0 { - firstX = glyph.X - } - // Cache glyph X offsets relative to the first glyph. - gids[i] = glyphInfo{ID: glyph.ID, X: glyph.X - firstX} - } - val := glyphValue[V]{ - glyphs: gids, - v: v, - } - c.cache.Put(key, val) -} - -type pathCache = glyphLRU[clip.PathSpec] - -type bitmapShapeCache = glyphLRU[op.CallOp] - -type glyphInfo struct { - ID GlyphID - X fixed.Int26_6 -} - -type layoutKey struct { - ppem fixed.Int26_6 - maxWidth, minWidth int - maxLines int - str string - truncator string - locale system.Locale - font giofont.Font - forceTruncate bool - wrapPolicy WrapPolicy - lineHeight fixed.Int26_6 - lineHeightScale float32 -} - -const maxSize = 1000 - -func gidsEqual(a []glyphInfo, glyphs []Glyph) bool { - if len(a) != len(glyphs) { - return false - } - firstX := fixed.Int26_6(0) - for i := range a { - if i == 0 { - firstX = glyphs[i].X - } - // Cache glyph X offsets relative to the first glyph. - if a[i].ID != glyphs[i].ID || a[i].X != (glyphs[i].X-firstX) { - return false - } - } - return true -} diff --git a/gio/text/lru_test.go b/gio/text/lru_test.go deleted file mode 100644 index 73e239b..0000000 --- a/gio/text/lru_test.go +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package text - -import ( - "strconv" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -func TestLayoutLRU(t *testing.T) { - c := new(layoutCache) - put := func(i int) { - c.Put(layoutKey{str: strconv.Itoa(i)}, document{}) - } - get := func(i int) bool { - _, ok := c.Get(layoutKey{str: strconv.Itoa(i)}) - return ok - } - testLRU(t, put, get) -} - -func TestPathLRU(t *testing.T) { - c := new(pathCache) - shaped := []Glyph{{ID: 1}} - put := func(i int) { - c.Put(uint64(i), shaped, clip.PathSpec{}) - } - get := func(i int) bool { - _, ok := c.Get(uint64(i), shaped) - return ok - } - testLRU(t, put, get) -} - -func testLRU(t *testing.T, put func(i int), get func(i int) bool) { - for i := range maxSize { - put(i) - } - for i := range maxSize { - if !get(i) { - t.Fatalf("key %d was evicted", i) - } - } - put(maxSize) - for i := 1; i < maxSize+1; i++ { - if !get(i) { - t.Fatalf("key %d was evicted", i) - } - } - if i := 0; get(i) { - t.Fatalf("key %d was not evicted", i) - } -} diff --git a/gio/text/shaper.go b/gio/text/shaper.go deleted file mode 100644 index 1fd645c..0000000 --- a/gio/text/shaper.go +++ /dev/null @@ -1,616 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package text - -import ( - "bufio" - "io" - "strings" - "unicode/utf8" - - giofont "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/go-text/typesetting/font" - "golang.org/x/image/math/fixed" -) - -// WrapPolicy configures strategies for choosing where to break lines of text for line -// wrapping. -type WrapPolicy uint8 - -const ( - // WrapHeuristically tries to minimize breaking within words (UAX#14 text segments) - // while also ensuring that text fits within the given MaxWidth. It will only break - // a line within a word (on a UAX#29 grapheme cluster boundary) when that word cannot - // fit on a line by itself. Additionally, when the final word of a line is being - // truncated, this policy will preserve as many symbols of that word as - // possible before the truncator. - WrapHeuristically WrapPolicy = iota - // WrapWords does not permit words (UAX#14 text segments) to be broken across lines. - // This means that sometimes long words will exceed the MaxWidth they are wrapped with. - WrapWords - // WrapGraphemes will maximize the amount of text on each line at the expense of readability, - // breaking any word across lines on UAX#29 grapheme cluster boundaries to maximize the number of - // grapheme clusters on each line. - WrapGraphemes -) - -// Parameters are static text shaping attributes applied to the entire shaped text. -type Parameters struct { - // Font describes the preferred typeface. - Font giofont.Font - // Alignment characterizes the positioning of text within the line. It does not directly - // impact shaping, but is provided in order to allow efficient offset computation. - Alignment Alignment - // PxPerEm is the pixels-per-em to shape the text with. - PxPerEm fixed.Int26_6 - // MaxLines limits the quantity of shaped lines. Zero means no limit. - MaxLines int - // Truncator is a string of text to insert where the shaped text was truncated, which - // can currently ohly happen if MaxLines is nonzero and the text on the final line is - // truncated. - Truncator string - - // WrapPolicy configures how line breaks will be chosen when wrapping text across lines. - WrapPolicy WrapPolicy - - // MinWidth and MaxWidth provide the minimum and maximum horizontal space constraints - // for the shaped text. - MinWidth, MaxWidth int - // Locale provides primary direction and language information for the shaped text. - Locale system.Locale - - // LineHeightScale is a scaling factor applied to the LineHeight of a paragraph. If zero, a default - // value of 1.2 will be used. - LineHeightScale float32 - - // LineHeight is the distance between the baselines of two lines of text. If zero, the PxPerEm - // of the any given paragraph will set the LineHeight of that paragraph. This value will be - // scaled by LineHeightScale, so applications desiring a specific fixed value - // should set LineHeightScale to 1. - LineHeight fixed.Int26_6 - - // forceTruncate controls whether the truncator string is inserted on the final line of - // text with a MaxLines. It is unexported because this behavior only makes sense for the - // shaper to control when it iterates paragraphs of text. - forceTruncate bool - - // DisableSpaceTrim prevents the width of the final whitespace glyph on a line from being zeroed. - // This is desirable for text editors (so that the whitespace can be selected), but is undesirable - // for ordinary display text. - DisableSpaceTrim bool -} - -type FontFace = giofont.FontFace - -// Glyph describes a shaped font glyph. Many fields are distances relative -// to the "dot", which is a point on the baseline (the line upon which glyphs -// visually rest) for the line of text containing the glyph. -// -// Glyphs are organized into "glyph clusters," which are sequences that -// may represent an arbitrary number of runes. -// -// Sequences of glyph clusters that share style parameters are grouped into "runs." -// -// "Document coordinates" are pixel values relative to the text's origin at (0,0) -// in the upper-left corner" Displaying each shaped glyph at the document -// coordinates of its dot will correctly visualize the text. -type Glyph struct { - // ID is a unique, per-shaper identifier for the shape of the glyph. - // Glyphs from the same shaper will share an ID when they are from - // the same face and represent the same glyph at the same size. - ID GlyphID - - // X is the x coordinate of the dot for this glyph in document coordinates. - X fixed.Int26_6 - // Y is the y coordinate of the dot for this glyph in document coordinates. - Y int32 - - // Advance is the logical width of the glyph. The glyph may be visually - // wider than this. - Advance fixed.Int26_6 - // Ascent is the distance from the dot to the logical top of glyphs in - // this glyph's face. The specific glyph may be shorter than this. - Ascent fixed.Int26_6 - // Descent is the distance from the dot to the logical bottom of glyphs - // in this glyph's face. The specific glyph may descend less than this. - Descent fixed.Int26_6 - // Offset encodes the origin of the drawing coordinate space for this glyph - // relative to the dot. This value is used when converting glyphs to paths. - Offset fixed.Point26_6 - // Bounds encodes the visual dimensions of the glyph relative to the dot. - Bounds fixed.Rectangle26_6 - // Runes is the number of runes represented by the glyph cluster this glyph - // belongs to. If Flags does not contain FlagClusterBreak, this value will - // always be zero. The final glyph in the cluster contains the runes count - // for the entire cluster. - Runes uint16 - // Flags encode special properties of this glyph. - Flags Flags -} - -type Flags uint16 - -const ( - // FlagTowardOrigin is set for glyphs in runs that flow - // towards the origin (RTL). - FlagTowardOrigin Flags = 1 << iota - // FlagLineBreak is set for the last glyph in a line. - FlagLineBreak - // FlagRunBreak is set for the last glyph in a run. A run is a sequence of - // glyphs sharing constant style properties (same size, same face, same - // direction, etc...). - FlagRunBreak - // FlagClusterBreak is set for the last glyph in a glyph cluster. A glyph cluster is a - // sequence of glyphs which are logically a single unit, but require multiple - // symbols from a font to display. - FlagClusterBreak - // FlagParagraphBreak indicates that the glyph cluster does not represent actual - // font glyphs, but was inserted by the shaper to represent line-breaking - // whitespace characters. After a glyph with FlagParagraphBreak set, the shaper - // will always return a glyph with FlagParagraphStart providing the X and Y - // coordinates of the start of the next line, even if that line has no contents. - FlagParagraphBreak - // FlagParagraphStart indicates that the glyph starts a new paragraph. - FlagParagraphStart - // FlagTruncator indicates that the glyph is part of a special truncator run that - // represents the portion of text removed due to truncation. A glyph with both - // FlagTruncator and FlagClusterBreak will have a Runes field accounting for all - // runes truncated. - FlagTruncator -) - -func (f Flags) String() string { - var b strings.Builder - if f&FlagParagraphStart != 0 { - b.WriteString("S") - } else { - b.WriteString("_") - } - if f&FlagParagraphBreak != 0 { - b.WriteString("P") - } else { - b.WriteString("_") - } - if f&FlagTowardOrigin != 0 { - b.WriteString("T") - } else { - b.WriteString("_") - } - if f&FlagLineBreak != 0 { - b.WriteString("L") - } else { - b.WriteString("_") - } - if f&FlagRunBreak != 0 { - b.WriteString("R") - } else { - b.WriteString("_") - } - if f&FlagClusterBreak != 0 { - b.WriteString("C") - } else { - b.WriteString("_") - } - if f&FlagTruncator != 0 { - b.WriteString("…") - } else { - b.WriteString("_") - } - return b.String() -} - -type GlyphID uint64 - -// Shaper converts strings of text into glyphs that can be displayed. The same -// Shaper should not be used in different goroutines. -// -// The Shaper controls text layout and has a cache, implemented as a map, and -// so laying out text in two different goroutines can easily result in -// concurrent access to said map, resulting in a panic. -// -// Practically speaking, this means you should use different Shapers for -// different top-level windows. -type Shaper struct { - config struct { - disableSystemFonts bool - collection []FontFace - } - initialized bool - shaper shaperImpl - pathCache pathCache - bitmapShapeCache bitmapShapeCache - layoutCache layoutCache - - reader *bufio.Reader - paragraph []byte - - // Iterator state. - brokeParagraph bool - pararagraphStart Glyph - txt document - line int - run int - glyph int - // advance is the width of glyphs from the current run that have already been displayed. - advance fixed.Int26_6 - // done tracks whether iteration is over. - done bool - err error -} - -// ShaperOptions configure text shapers. -type ShaperOption func(*Shaper) - -// NoSystemFonts can be used to disable system font loading. -func NoSystemFonts() ShaperOption { - return func(s *Shaper) { - s.config.disableSystemFonts = true - } -} - -// WithCollection can be used to provide a collection of pre-loaded fonts to the shaper. -func WithCollection(collection []FontFace) ShaperOption { - return func(s *Shaper) { - s.config.collection = collection - } -} - -// NewShaper constructs a shaper with the provided options. -// -// NewShaper must be called after [app.NewWindow], unless the [NoSystemFonts] -// option is specified. This is an unfortunate restriction caused by some platforms -// such as Android. -func NewShaper(options ...ShaperOption) *Shaper { - l := &Shaper{} - for _, opt := range options { - opt(l) - } - l.init() - return l -} - -func (l *Shaper) init() { - if l.initialized { - return - } - l.initialized = true - l.reader = bufio.NewReader(nil) - l.shaper = *newShaperImpl(!l.config.disableSystemFonts, l.config.collection) -} - -// Layout text from an io.Reader according to a set of options. Results can be retrieved by -// iteratively calling NextGlyph. -func (l *Shaper) Layout(params Parameters, txt io.Reader) { - l.init() - l.layoutText(params, txt, "") -} - -// LayoutString is Layout for strings. -func (l *Shaper) LayoutString(params Parameters, str string) { - l.init() - l.layoutText(params, nil, str) -} - -func (l *Shaper) reset(align Alignment) { - l.line, l.run, l.glyph, l.advance = 0, 0, 0, 0 - l.done = false - l.txt.reset() - l.txt.alignment = align -} - -// layoutText lays out a large text document by breaking it into paragraphs and laying -// out each of them separately. This allows the shaping results to be cached independently -// by paragraph. Only one of txt and str should be provided. -func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) { - l.reset(params.Alignment) - if txt == nil && len(str) == 0 { - l.txt.append(l.layoutParagraph(params, "", nil)) - return - } - l.reader.Reset(txt) - truncating := params.MaxLines > 0 - var done bool - var endByte int - for !done { - l.paragraph = l.paragraph[:0] - if txt != nil { - for { - b, err := l.reader.ReadByte() - if err != nil { - // EOF or any other error ends processing here. - done = true - break - } - l.paragraph = append(l.paragraph, b) - if b == '\n' { - break - } - } - if !done { - _, re := l.reader.ReadByte() - done = re != nil - if !done { - _ = l.reader.UnreadByte() - } - } - } else { - idx := strings.IndexByte(str, '\n') - if idx == -1 { - done = true - endByte = len(str) - } else { - endByte = idx + 1 - done = endByte == len(str) - } - } - if len(str[:endByte]) > 0 || (len(l.paragraph) > 0 || len(l.txt.lines) == 0) { - params.forceTruncate = truncating && !done - lines := l.layoutParagraph(params, str[:endByte], l.paragraph) - if truncating { - params.MaxLines -= len(lines.lines) - if params.MaxLines == 0 { - done = true - // We've truncated the text, but we need to account for all of the runes we never - // decoded in the truncator. - var unreadRunes int - if txt == nil { - unreadRunes = utf8.RuneCountInString(str[endByte:]) - } else { - for { - _, _, e := l.reader.ReadRune() - if e != nil { - break - } - unreadRunes++ - } - } - l.txt.unreadRuneCount = unreadRunes - } - } - l.txt.append(lines) - } - if done { - return - } - str = str[endByte:] - } -} - -// layoutParagraph shapes and wraps a paragraph using the provided parameters. -// It accepts the paragraph data in either string or rune format, preferring the -// string in order to hit the shaper cache more quickly. -func (l *Shaper) layoutParagraph(params Parameters, asStr string, asBytes []byte) document { - if l == nil { - return document{} - } - if len(asStr) == 0 && len(asBytes) > 0 { - asStr = string(asBytes) - } - // Alignment is not part of the cache key because changing it does not impact shaping. - lk := layoutKey{ - ppem: params.PxPerEm, - maxWidth: params.MaxWidth, - minWidth: params.MinWidth, - maxLines: params.MaxLines, - truncator: params.Truncator, - locale: params.Locale, - font: params.Font, - forceTruncate: params.forceTruncate, - wrapPolicy: params.WrapPolicy, - str: asStr, - lineHeight: params.LineHeight, - lineHeightScale: params.LineHeightScale, - } - if l, ok := l.layoutCache.Get(lk); ok { - return l - } - lines := l.shaper.LayoutRunes(params, []rune(asStr)) - l.layoutCache.Put(lk, lines) - return lines -} - -// NextGlyph returns the next glyph from the most recent shaping operation, if -// any. If there are no more glyphs, ok will be false. -func (l *Shaper) NextGlyph() (_ Glyph, ok bool) { - l.init() - if l.done { - return Glyph{}, false - } - for { - if l.line == len(l.txt.lines) { - if l.brokeParagraph { - l.brokeParagraph = false - return l.pararagraphStart, true - } - if l.err == nil { - l.err = io.EOF - } - return Glyph{}, false - } - line := l.txt.lines[l.line] - if l.run == len(line.runs) { - l.line++ - l.run = 0 - continue - } - run := line.runs[l.run] - align := l.txt.alignment.Align(line.direction, line.width, l.txt.alignWidth) - if l.line == 0 && l.run == 0 && len(run.Glyphs) == 0 { - // The very first run is empty, which will only happen when the - // entire text is a shaped empty string. Return a single synthetic - // glyph to provide ascent/descent information to the caller. - l.done = true - return Glyph{ - X: align, - Y: int32(line.yOffset), - Runes: 0, - Flags: FlagLineBreak | FlagClusterBreak | FlagRunBreak, - Ascent: line.ascent, - Descent: line.descent, - }, true - } - if l.glyph == len(run.Glyphs) { - l.run++ - l.glyph = 0 - l.advance = 0 - continue - } - glyphIdx := l.glyph - rtl := run.Direction.Progression() == system.TowardOrigin - if rtl { - // If RTL, traverse glyphs backwards to ensure rune order. - glyphIdx = len(run.Glyphs) - 1 - glyphIdx - } - g := run.Glyphs[glyphIdx] - if rtl { - // Modify the advance prior to computing runOffset to ensure that the - // current glyph's width is subtracted in RTL. - l.advance += g.xAdvance - } - // runOffset computes how far into the run the dot should be positioned. - runOffset := l.advance - if rtl { - runOffset = run.Advance - l.advance - } - glyph := Glyph{ - ID: g.id, - X: align + run.X + runOffset, - Y: int32(line.yOffset), - Ascent: line.ascent, - Descent: line.descent, - Advance: g.xAdvance, - Runes: uint16(g.runeCount), - Offset: fixed.Point26_6{ - X: g.xOffset, - Y: g.yOffset, - }, - Bounds: g.bounds, - } - if run.truncator { - glyph.Flags |= FlagTruncator - } - l.glyph++ - if !rtl { - l.advance += g.xAdvance - } - - endOfRun := l.glyph == len(run.Glyphs) - if endOfRun { - glyph.Flags |= FlagRunBreak - } - endOfLine := endOfRun && l.run == len(line.runs)-1 - if endOfLine { - glyph.Flags |= FlagLineBreak - } - endOfText := endOfLine && l.line == len(l.txt.lines)-1 - nextGlyph := l.glyph - if rtl { - nextGlyph = len(run.Glyphs) - 1 - nextGlyph - } - endOfCluster := endOfRun || run.Glyphs[nextGlyph].clusterIndex != g.clusterIndex - if run.truncator { - // Only emit a single cluster for the entire truncator sequence. - endOfCluster = endOfRun - } - if endOfCluster { - glyph.Flags |= FlagClusterBreak - if run.truncator { - glyph.Runes += uint16(l.txt.unreadRuneCount) - } - } else { - glyph.Runes = 0 - } - if run.Direction.Progression() == system.TowardOrigin { - glyph.Flags |= FlagTowardOrigin - } - if l.brokeParagraph { - glyph.Flags |= FlagParagraphStart - l.brokeParagraph = false - } - if g.glyphCount == 0 { - glyph.Flags |= FlagParagraphBreak - l.brokeParagraph = true - if endOfText { - l.pararagraphStart = Glyph{ - Ascent: glyph.Ascent, - Descent: glyph.Descent, - Flags: FlagParagraphStart | FlagLineBreak | FlagRunBreak | FlagClusterBreak, - } - // If a glyph is both a paragraph break and the final glyph, it's a newline - // at the end of the text. We must inform widgets like the text editor - // of a valid cursor position they can use for "after" such a newline, - // taking text alignment into account. - l.pararagraphStart.X = l.txt.alignment.Align(line.direction, 0, l.txt.alignWidth) - l.pararagraphStart.Y = glyph.Y + int32(line.lineHeight.Round()) - } - } - return glyph, true - } -} - -const ( - facebits = 16 - sizebits = 16 - gidbits = 64 - facebits - sizebits -) - -// newGlyphID encodes a face and a glyph id into a GlyphID. -func newGlyphID(ppem fixed.Int26_6, faceIdx int, gid font.GID) GlyphID { - if gid&^((1<> (gidbits + sizebits)) - ppem := fixed.Int26_6((g & ((1<> gidbits) - gid := font.GID(g) & (1< 0; i-- { - t.Run(fmt.Sprintf("truncated to %d/%d lines", i, untruncatedCount), func(t *testing.T) { - cache.LayoutString(Parameters{ - Alignment: Middle, - PxPerEm: fixed.I(10), - MaxLines: i, - MinWidth: 200, - MaxWidth: 200, - Locale: english, - }, textInput) - lineCount := 0 - lastGlyphWasLineBreak := false - glyphs := []Glyph{} - untruncatedRunes := 0 - truncatedRunes := 0 - for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() { - glyphs = append(glyphs, g) - if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 { - truncatedRunes += int(g.Runes) - } else { - untruncatedRunes += int(g.Runes) - } - if g.Flags&FlagLineBreak != 0 { - lineCount++ - lastGlyphWasLineBreak = true - } else { - lastGlyphWasLineBreak = false - } - } - if lastGlyphWasLineBreak && truncatedRunes == 0 { - // There was no actual line of text following this break. - lineCount-- - } - if i <= untruncatedCount { - if lineCount != i { - t.Errorf("expected %d lines, got %d", i, lineCount) - } - } else if i > untruncatedCount { - if lineCount != untruncatedCount { - t.Errorf("expected %d lines, got %d", untruncatedCount, lineCount) - } - } - if expected := len([]rune(textInput)); truncatedRunes+untruncatedRunes != expected { - t.Errorf("expected %d total runes, got %d (%d truncated)", expected, truncatedRunes+untruncatedRunes, truncatedRunes) - } - }) - } -} - -// TestWrappingForcedTruncation checks that the line wrapper's truncation features -// activate correctly on multi-paragraph text when later paragraphs are truncated. -func TestWrappingForcedTruncation(t *testing.T) { - // Use a test string containing multiple newlines to ensure that they are shaped - // as separate paragraphs. - textInput := "Lorem ipsum\ndolor sit\namet" - ltrFace, _ := opentype.Parse(goregular.TTF) - collection := []FontFace{{Face: ltrFace}} - cache := NewShaper(NoSystemFonts(), WithCollection(collection)) - cache.LayoutString(Parameters{ - Alignment: Middle, - PxPerEm: fixed.I(10), - MinWidth: 200, - MaxWidth: 200, - Locale: english, - }, textInput) - untruncatedCount := len(cache.txt.lines) - - for i := untruncatedCount + 1; i > 0; i-- { - t.Run(fmt.Sprintf("truncated to %d/%d lines", i, untruncatedCount), func(t *testing.T) { - cache.LayoutString(Parameters{ - Alignment: Middle, - PxPerEm: fixed.I(10), - MaxLines: i, - MinWidth: 200, - MaxWidth: 200, - Locale: english, - }, textInput) - lineCount := 0 - glyphs := []Glyph{} - untruncatedRunes := 0 - truncatedRunes := 0 - for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() { - glyphs = append(glyphs, g) - if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 { - truncatedRunes += int(g.Runes) - } else { - untruncatedRunes += int(g.Runes) - } - if g.Flags&FlagLineBreak != 0 { - lineCount++ - } - } - expectedTruncated := false - expectedLines := 0 - if i < untruncatedCount { - expectedLines = i - expectedTruncated = true - } else if i == untruncatedCount { - expectedLines = i - expectedTruncated = false - } else if i > untruncatedCount { - expectedLines = untruncatedCount - expectedTruncated = false - } - if lineCount != expectedLines { - t.Errorf("expected %d lines, got %d", expectedLines, lineCount) - } - if truncatedRunes > 0 != expectedTruncated { - t.Errorf("expected expectedTruncated=%v, truncatedRunes=%d", expectedTruncated, truncatedRunes) - } - if expected := len([]rune(textInput)); truncatedRunes+untruncatedRunes != expected { - t.Errorf("expected %d total runes, got %d (%d truncated)", expected, truncatedRunes+untruncatedRunes, truncatedRunes) - } - }) - } -} - -// TestShapingNewlineHandling checks that the shaper's newline splitting behaves -// consistently and does not create spurious lines of text. -func TestShapingNewlineHandling(t *testing.T) { - type testcase struct { - textInput string - expectedLines int - expectedGlyphs int - maxLines int - expectedTruncated int - } - for _, tc := range []testcase{ - {textInput: "a\n", expectedLines: 1, expectedGlyphs: 3}, - {textInput: "a\nb", expectedLines: 2, expectedGlyphs: 3}, - {textInput: "", expectedLines: 1, expectedGlyphs: 1}, - {textInput: "\n", expectedLines: 1, expectedGlyphs: 2}, - {textInput: "\n\n", expectedLines: 2, expectedGlyphs: 3}, - {textInput: "\n\n\n", expectedLines: 3, expectedGlyphs: 4}, - {textInput: "\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 1}, - {textInput: "\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 2}, - {textInput: "\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 3}, - {textInput: "a\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 1}, - {textInput: "a\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 2}, - {textInput: "a\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 3}, - {textInput: "\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 2}, - {textInput: "\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 1}, - {textInput: "\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 2}, - {textInput: "a\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 3}, - {textInput: "a\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 1}, - {textInput: "a\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 2}, - } { - t.Run(fmt.Sprintf("%q-maxLines%d", tc.textInput, tc.maxLines), func(t *testing.T) { - ltrFace, _ := opentype.Parse(goregular.TTF) - collection := []FontFace{{Face: ltrFace}} - cache := NewShaper(NoSystemFonts(), WithCollection(collection)) - checkGlyphs := func() { - glyphs := []Glyph{} - runes := 0 - truncated := 0 - for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() { - glyphs = append(glyphs, g) - if g.Flags&FlagTruncator == 0 { - runes += int(g.Runes) - } else { - truncated += int(g.Runes) - } - } - if expected := len([]rune(tc.textInput)) - tc.expectedTruncated; expected != runes { - t.Errorf("expected %d runes, got %d", expected, runes) - } - if truncated != tc.expectedTruncated { - t.Errorf("expected %d truncated runes, got %d", tc.expectedTruncated, truncated) - } - if len(glyphs) != tc.expectedGlyphs { - t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs)) - } - findBreak := func(g Glyph) bool { - return g.Flags&FlagParagraphBreak != 0 - } - found := 0 - for idx := slices.IndexFunc(glyphs, findBreak); idx != -1; idx = slices.IndexFunc(glyphs, findBreak) { - found++ - breakGlyph := glyphs[idx] - startGlyph := glyphs[idx+1] - glyphs = glyphs[idx+1:] - if flags := breakGlyph.Flags; flags&FlagParagraphBreak == 0 { - t.Errorf("expected newline glyph to have P flag, got %s", flags) - } - if flags := startGlyph.Flags; flags&FlagParagraphStart == 0 { - t.Errorf("expected newline glyph to have S flag, got %s", flags) - } - breakX, breakY := breakGlyph.X, breakGlyph.Y - startX, startY := startGlyph.X, startGlyph.Y - if breakX == startX && idx != 0 { - t.Errorf("expected paragraph start glyph to have cursor x, got %v", startX) - } - if breakY == startY { - t.Errorf("expected paragraph start glyph to have cursor y") - } - } - if count := strings.Count(tc.textInput, "\n"); found != count && tc.maxLines == 0 { - t.Errorf("expected %d paragraph breaks, found %d", count, found) - } else if tc.maxLines > 0 && found > tc.maxLines { - t.Errorf("expected %d paragraph breaks due to truncation, found %d", tc.maxLines, found) - } - } - params := Parameters{ - Alignment: Middle, - PxPerEm: fixed.I(10), - MinWidth: 200, - MaxWidth: 200, - Locale: english, - MaxLines: tc.maxLines, - } - cache.LayoutString(params, tc.textInput) - if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines { - t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount) - } - checkGlyphs() - - cache.Layout(params, strings.NewReader(tc.textInput)) - if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines { - t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount) - } - checkGlyphs() - }) - } -} - -// TestCacheEmptyString ensures that shaping the empty string returns a -// single synthetic glyph with ascent/descent info. -func TestCacheEmptyString(t *testing.T) { - ltrFace, _ := opentype.Parse(goregular.TTF) - collection := []FontFace{{Face: ltrFace}} - cache := NewShaper(NoSystemFonts(), WithCollection(collection)) - cache.LayoutString(Parameters{ - Alignment: Middle, - PxPerEm: fixed.I(10), - MinWidth: 200, - MaxWidth: 200, - Locale: english, - }, "") - glyphs := make([]Glyph, 0, 1) - for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() { - glyphs = append(glyphs, g) - } - if len(glyphs) != 1 { - t.Errorf("expected %d glyphs, got %d", 1, len(glyphs)) - } - glyph := glyphs[0] - checkFlag(t, true, FlagClusterBreak, glyph, 0) - checkFlag(t, true, FlagRunBreak, glyph, 0) - checkFlag(t, true, FlagLineBreak, glyph, 0) - checkFlag(t, false, FlagParagraphBreak, glyph, 0) - if glyph.Ascent == 0 { - t.Errorf("expected non-zero ascent") - } - if glyph.Descent == 0 { - t.Errorf("expected non-zero descent") - } - if glyph.Y == 0 { - t.Errorf("expected non-zero y offset") - } - if glyph.X == 0 { - t.Errorf("expected non-zero x offset") - } -} - -// TestCacheAlignment ensures that shaping with different alignments or dominant -// text directions results in different X offsets. -func TestCacheAlignment(t *testing.T) { - ltrFace, _ := opentype.Parse(goregular.TTF) - collection := []FontFace{{Face: ltrFace}} - cache := NewShaper(NoSystemFonts(), WithCollection(collection)) - params := Parameters{ - Alignment: Start, - PxPerEm: fixed.I(10), - MinWidth: 200, - MaxWidth: 200, - Locale: english, - } - cache.LayoutString(params, "A") - glyph, _ := cache.NextGlyph() - startX := glyph.X - params.Alignment = Middle - cache.LayoutString(params, "A") - glyph, _ = cache.NextGlyph() - middleX := glyph.X - params.Alignment = End - cache.LayoutString(params, "A") - glyph, _ = cache.NextGlyph() - endX := glyph.X - if startX == middleX || startX == endX || endX == middleX { - t.Errorf("[LTR] shaping with with different alignments should not produce the same X, start %d, middle %d, end %d", startX, middleX, endX) - } - params.Locale = arabic - params.Alignment = Start - cache.LayoutString(params, "A") - glyph, _ = cache.NextGlyph() - rtlStartX := glyph.X - params.Alignment = Middle - cache.LayoutString(params, "A") - glyph, _ = cache.NextGlyph() - rtlMiddleX := glyph.X - params.Alignment = End - cache.LayoutString(params, "A") - glyph, _ = cache.NextGlyph() - rtlEndX := glyph.X - if rtlStartX == rtlMiddleX || rtlStartX == rtlEndX || rtlEndX == rtlMiddleX { - t.Errorf("[RTL] shaping with with different alignments should not produce the same X, start %d, middle %d, end %d", rtlStartX, rtlMiddleX, rtlEndX) - } - if startX == rtlStartX || endX == rtlEndX { - t.Errorf("shaping with with different dominant text directions and the same alignment should not produce the same X unless it's middle-aligned") - } -} - -func TestCacheGlyphConverstion(t *testing.T) { - ltrFace, _ := opentype.Parse(goregular.TTF) - rtlFace, _ := opentype.Parse(nsareg.TTF) - collection := []FontFace{{Face: ltrFace}, {Face: rtlFace}} - type testcase struct { - name string - text string - locale system.Locale - expected []Glyph - } - for _, tc := range []testcase{ - { - name: "bidi ltr", - text: "The quick سماء שלום لا fox تمط שלום\nغير the\nlazy dog.", - locale: english, - }, - { - name: "bidi rtl", - text: "الحب سماء brown привет fox تمط jumps\nпривет over\nغير الأحلام.", - locale: arabic, - }, - } { - t.Run(tc.name, func(t *testing.T) { - cache := NewShaper(NoSystemFonts(), WithCollection(collection)) - cache.LayoutString(Parameters{ - PxPerEm: fixed.I(10), - MaxWidth: 200, - Locale: tc.locale, - }, tc.text) - doc := cache.txt - glyphs := make([]Glyph, 0, len(tc.expected)) - for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() { - glyphs = append(glyphs, g) - } - glyphCursor := 0 - for _, line := range doc.lines { - for runIdx, run := range line.runs { - lastRun := runIdx == len(line.runs)-1 - start := 0 - end := len(run.Glyphs) - 1 - inc := 1 - towardOrigin := false - if run.Direction.Progression() == system.TowardOrigin { - start = len(run.Glyphs) - 1 - end = 0 - inc = -1 - towardOrigin = true - } - for glyphIdx := start; ; glyphIdx += inc { - endOfRun := glyphIdx == end - glyph := run.Glyphs[glyphIdx] - endOfCluster := glyphIdx == end || run.Glyphs[glyphIdx+inc].clusterIndex != glyph.clusterIndex - - actual := glyphs[glyphCursor] - if actual.ID != glyph.id { - t.Errorf("glyphs[%d] expected id %d, got id %d", glyphCursor, glyph.id, actual.ID) - } - // Synthetic glyphs should only ever show up at the end of lines. - endOfLine := lastRun && endOfRun - synthetic := glyph.glyphCount == 0 && endOfLine - checkFlag(t, endOfLine, FlagLineBreak, actual, glyphCursor) - checkFlag(t, endOfRun, FlagRunBreak, actual, glyphCursor) - checkFlag(t, towardOrigin, FlagTowardOrigin, actual, glyphCursor) - checkFlag(t, synthetic, FlagParagraphBreak, actual, glyphCursor) - checkFlag(t, endOfCluster, FlagClusterBreak, actual, glyphCursor) - glyphCursor++ - if glyphIdx == end { - break - } - } - } - } - - printLinePositioning(t, doc.lines, glyphs) - }) - } -} - -func checkFlag(t *testing.T, shouldHave bool, flag Flags, actual Glyph, glyphCursor int) { - t.Helper() - if shouldHave && actual.Flags&flag == 0 { - t.Errorf("glyphs[%d] should have %s set", glyphCursor, flag) - } else if !shouldHave && actual.Flags&flag != 0 { - t.Errorf("glyphs[%d] should not have %s set", glyphCursor, flag) - } -} - -func printLinePositioning(t *testing.T, lines []line, glyphs []Glyph) { - t.Helper() - glyphCursor := 0 - for i, line := range lines { - t.Logf("line %d, dir %s, width %d, visual %v, runeCount: %d", i, line.direction, line.width, line.visualOrder, line.runeCount) - for k, run := range line.runs { - t.Logf("run: %d, dir %s, width %d, runes {count: %d, offset: %d}", k, run.Direction, run.Advance, run.Runes.Count, run.Runes.Offset) - start := 0 - end := len(run.Glyphs) - 1 - inc := 1 - if run.Direction.Progression() == system.TowardOrigin { - start = len(run.Glyphs) - 1 - end = 0 - inc = -1 - } - for g := start; ; g += inc { - glyph := run.Glyphs[g] - if glyphCursor < len(glyphs) { - t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - glyphs[%2d] flags %s", g, glyph.xAdvance, glyph.runeCount, glyph.glyphCount, glyphCursor, glyphs[glyphCursor].Flags) - t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - n/a", g, glyph.xAdvance, glyph.runeCount, glyph.glyphCount) - } - glyphCursor++ - if g == end { - break - } - } - } - } -} - -// TestShapeStringRuneAccounting tries shaping the same string/parameter combinations with both -// shaping methods and ensures that the resulting glyph stream always has the right number of -// runes accounted for. -func TestShapeStringRuneAccounting(t *testing.T) { - type testcase struct { - name string - input string - params Parameters - } - type setup struct { - kind string - do func(*Shaper, Parameters, string) - } - for _, tc := range []testcase{ - { - name: "simple truncated", - input: "abc", - params: Parameters{ - PxPerEm: fixed.Int26_6(16), - MaxWidth: 100, - MaxLines: 1, - }, - }, - { - name: "simple", - input: "abc", - params: Parameters{ - PxPerEm: fixed.Int26_6(16), - MaxWidth: 100, - }, - }, - { - name: "newline regression", - input: "\n", - params: Parameters{ - Font: font.Font{Typeface: "Go", Style: font.Regular, Weight: font.Normal}, - Alignment: Start, - PxPerEm: 768, - MaxLines: 1, - Truncator: "\u200b", - WrapPolicy: WrapHeuristically, - MaxWidth: 999929, - }, - }, - { - name: "newline zero-width regression", - input: "\n", - params: Parameters{ - Font: font.Font{Typeface: "Go", Style: font.Regular, Weight: font.Normal}, - Alignment: Start, - PxPerEm: 768, - MaxLines: 1, - Truncator: "\u200b", - WrapPolicy: WrapHeuristically, - MaxWidth: 0, - }, - }, - { - name: "double newline regression", - input: "\n\n", - params: Parameters{ - Font: font.Font{Typeface: "Go", Style: font.Regular, Weight: font.Normal}, - Alignment: Start, - PxPerEm: 768, - MaxLines: 1, - Truncator: "\u200b", - WrapPolicy: WrapHeuristically, - MaxWidth: 1000, - }, - }, - { - name: "triple newline regression", - input: "\n\n\n", - params: Parameters{ - Font: font.Font{Typeface: "Go", Style: font.Regular, Weight: font.Normal}, - Alignment: Start, - PxPerEm: 768, - MaxLines: 1, - Truncator: "\u200b", - WrapPolicy: WrapHeuristically, - MaxWidth: 1000, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - for _, setup := range []setup{ - { - kind: "LayoutString", - do: func(shaper *Shaper, params Parameters, input string) { - shaper.LayoutString(params, input) - }, - }, - { - kind: "Layout", - do: func(shaper *Shaper, params Parameters, input string) { - shaper.Layout(params, strings.NewReader(input)) - }, - }, - } { - t.Run(setup.kind, func(t *testing.T) { - shaper := NewShaper(NoSystemFonts(), WithCollection(gofont.Collection())) - setup.do(shaper, tc.params, tc.input) - - glyphs := []Glyph{} - for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { - glyphs = append(glyphs, g) - } - totalRunes := 0 - for _, g := range glyphs { - totalRunes += int(g.Runes) - } - if inputRunes := len([]rune(tc.input)); totalRunes != inputRunes { - t.Errorf("input contained %d runes, but glyphs contained %d", inputRunes, totalRunes) - } - }) - } - }) - } -} diff --git a/gio/text/text.go b/gio/text/text.go deleted file mode 100644 index 684a16b..0000000 --- a/gio/text/text.go +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package text - -import ( - "fmt" - - "github.com/p9c/p9/pkg/gel/gio/io/system" - "golang.org/x/image/math/fixed" -) - -type Alignment uint8 - -const ( - Start Alignment = iota - End - Middle -) - -func (a Alignment) String() string { - switch a { - case Start: - return "Start" - case End: - return "End" - case Middle: - return "Middle" - default: - panic("invalid Alignment") - } -} - -// Align returns the x offset that should be applied to text with width so that it -// appears correctly aligned within a space of size maxWidth and with the primary -// text direction dir. -func (a Alignment) Align(dir system.TextDirection, width fixed.Int26_6, maxWidth int) fixed.Int26_6 { - mw := fixed.I(maxWidth) - if dir.Progression() == system.TowardOrigin { - switch a { - case Start: - a = End - case End: - a = Start - } - } - switch a { - case Middle: - return (mw - width) / 2 - case End: - return (mw - width) - case Start: - return 0 - default: - panic(fmt.Errorf("unknown alignment %v", a)) - } -} diff --git a/gio/unit/unit.go b/gio/unit/unit.go deleted file mode 100644 index 1cb46d3..0000000 --- a/gio/unit/unit.go +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -/* -Package unit implements device independent units. - -Device independent pixel, or dp, is the unit for sizes independent of -the underlying display device. - -Scaled pixels, or sp, is the unit for text sizes. An sp is like dp with -text scaling applied. - -Finally, pixels, or px, is the unit for display dependent pixels. Their -size vary between platforms and displays. - -To maintain a constant visual size across platforms and displays, always -use dps or sps to define user interfaces. Only use pixels for derived -values. -*/ -package unit - -import ( - "math" -) - -// Metric converts Values to device-dependent pixels, px. The zero -// value represents a 1-to-1 scale from dp, sp to pixels. -type Metric struct { - // PxPerDp is the device-dependent pixels per dp. - PxPerDp float32 - // PxPerSp is the device-dependent pixels per sp. - PxPerSp float32 -} - -type ( - // Dp represents device independent pixels. 1 dp will - // have the same apparent size across platforms and - // display resolutions. - Dp float32 - // Sp is like UnitDp but for font sizes. - Sp float32 -) - -// Dp converts v to pixels, rounded to the nearest integer value. -func (c Metric) Dp(v Dp) int { - return int(math.Round(float64(nonZero(c.PxPerDp)) * float64(v))) -} - -// Sp converts v to pixels, rounded to the nearest integer value. -func (c Metric) Sp(v Sp) int { - return int(math.Round(float64(nonZero(c.PxPerSp)) * float64(v))) -} - -// DpToSp converts v dp to sp. -func (c Metric) DpToSp(v Dp) Sp { - return Sp(float32(v) * nonZero(c.PxPerDp) / nonZero(c.PxPerSp)) -} - -// SpToDp converts v sp to dp. -func (c Metric) SpToDp(v Sp) Dp { - return Dp(float32(v) * nonZero(c.PxPerSp) / nonZero(c.PxPerDp)) -} - -// PxToSp converts v px to sp. -func (c Metric) PxToSp(v int) Sp { - return Sp(float32(v) / nonZero(c.PxPerSp)) -} - -// PxToDp converts v px to dp. -func (c Metric) PxToDp(v int) Dp { - return Dp(float32(v) / nonZero(c.PxPerDp)) -} - -func nonZero(v float32) float32 { - if v == 0. { - return 1 - } - return v -} diff --git a/gio/unit/unit_test.go b/gio/unit/unit_test.go deleted file mode 100644 index e79dc9c..0000000 --- a/gio/unit/unit_test.go +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package unit_test - -import ( - "testing" - - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -func TestMetric_DpToSp(t *testing.T) { - m := unit.Metric{ - PxPerDp: 2, - PxPerSp: 3, - } - - { - exp := m.Dp(5) - got := m.Sp(m.DpToSp(5)) - if got != exp { - t.Errorf("DpToSp conversion mismatch %v != %v", exp, got) - } - } - - { - exp := m.Sp(5) - got := m.Dp(m.SpToDp(5)) - if got != exp { - t.Errorf("SpToDp conversion mismatch %v != %v", exp, got) - } - } - - { - exp := unit.Dp(5) - got := m.PxToDp(m.Dp(5)) - if got != exp { - t.Errorf("PxToDp conversion mismatch %v != %v", exp, got) - } - } - - { - exp := unit.Sp(5) - got := m.PxToSp(m.Sp(5)) - if got != exp { - t.Errorf("PxToSp conversion mismatch %v != %v", exp, got) - } - } -} diff --git a/gio/widget/bool.go b/gio/widget/bool.go deleted file mode 100644 index afd8602..0000000 --- a/gio/widget/bool.go +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/layout" -) - -type Bool struct { - Value bool - - clk Clickable -} - -// Update the widget state and report whether Value was changed. -func (b *Bool) Update(gtx layout.Context) bool { - changed := false - for b.clk.clicked(b, gtx) { - b.Value = !b.Value - changed = true - } - return changed -} - -// Hovered reports whether pointer is over the element. -func (b *Bool) Hovered() bool { - return b.clk.Hovered() -} - -// Pressed reports whether pointer is pressing the element. -func (b *Bool) Pressed() bool { - return b.clk.Pressed() -} - -func (b *Bool) History() []Press { - return b.clk.History() -} - -func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { - b.Update(gtx) - dims := b.clk.layout(b, gtx, func(gtx layout.Context) layout.Dimensions { - semantic.SelectedOp(b.Value).Add(gtx.Ops) - semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) - return w(gtx) - }) - return dims -} diff --git a/gio/widget/border.go b/gio/widget/border.go deleted file mode 100644 index f45b20d..0000000 --- a/gio/widget/border.go +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// Border lays out a widget and draws a border inside it. -type Border struct { - Color color.NRGBA - CornerRadius unit.Dp - Width unit.Dp -} - -func (b Border) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { - dims := w(gtx) - sz := dims.Size - - rr := gtx.Dp(b.CornerRadius) - width := gtx.Dp(b.Width) - whalf := (width + 1) / 2 - sz.X -= whalf * 2 - sz.Y -= whalf * 2 - - r := image.Rectangle{Max: sz} - r = r.Add(image.Point{X: whalf, Y: whalf}) - - paint.FillShape(gtx.Ops, - b.Color, - clip.Stroke{ - Path: clip.UniformRRect(r, rr).Path(gtx.Ops), - Width: float32(width), - }.Op(), - ) - - return dims -} diff --git a/gio/widget/buffer.go b/gio/widget/buffer.go deleted file mode 100644 index 2a3961d..0000000 --- a/gio/widget/buffer.go +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "io" - "unicode/utf8" - - "golang.org/x/text/runes" -) - -// editBuffer implements a gap buffer for text editing. -type editBuffer struct { - // The gap start and end in bytes. - gapstart, gapend int - text []byte - - // changed tracks whether the buffer content - // has changed since the last call to Changed. - changed bool -} - -var _ textSource = (*editBuffer)(nil) - -const minSpace = 5 - -func (e *editBuffer) Changed() bool { - c := e.changed - e.changed = false - return c -} - -func (e *editBuffer) deleteRunes(caret, count int) (bytes int, runes int) { - e.moveGap(caret, 0) - for ; count < 0 && e.gapstart > 0; count++ { - _, s := utf8.DecodeLastRune(e.text[:e.gapstart]) - e.gapstart -= s - bytes += s - runes++ - e.changed = e.changed || s > 0 - } - for ; count > 0 && e.gapend < len(e.text); count-- { - _, s := utf8.DecodeRune(e.text[e.gapend:]) - e.gapend += s - e.changed = e.changed || s > 0 - } - return -} - -// moveGap moves the gap to the caret position. After returning, -// the gap is guaranteed to be at least space bytes long. -func (e *editBuffer) moveGap(caret, space int) { - if e.gapLen() < space { - if space < minSpace { - space = minSpace - } - txt := make([]byte, int(e.Size())+space) - // Expand to capacity. - txt = txt[:cap(txt)] - gaplen := len(txt) - int(e.Size()) - if caret > e.gapstart { - copy(txt, e.text[:e.gapstart]) - copy(txt[caret+gaplen:], e.text[caret:]) - copy(txt[e.gapstart:], e.text[e.gapend:caret+e.gapLen()]) - } else { - copy(txt, e.text[:caret]) - copy(txt[e.gapstart+gaplen:], e.text[e.gapend:]) - copy(txt[caret+gaplen:], e.text[caret:e.gapstart]) - } - e.text = txt - e.gapstart = caret - e.gapend = e.gapstart + gaplen - } else { - if caret > e.gapstart { - copy(e.text[e.gapstart:], e.text[e.gapend:caret+e.gapLen()]) - } else { - copy(e.text[caret+e.gapLen():], e.text[caret:e.gapstart]) - } - l := e.gapLen() - e.gapstart = caret - e.gapend = e.gapstart + l - } -} - -func (e *editBuffer) Size() int64 { - return int64(len(e.text) - e.gapLen()) -} - -func (e *editBuffer) gapLen() int { - return e.gapend - e.gapstart -} - -func (e *editBuffer) ReadAt(p []byte, offset int64) (int, error) { - if len(p) == 0 { - return 0, nil - } - if offset == e.Size() { - return 0, io.EOF - } - var total int - if offset < int64(e.gapstart) { - n := copy(p, e.text[offset:e.gapstart]) - p = p[n:] - total += n - offset += int64(n) - } - if offset >= int64(e.gapstart) { - n := copy(p, e.text[offset+int64(e.gapLen()):]) - total += n - } - return total, nil -} - -func (e *editBuffer) ReplaceRunes(byteOffset, runeCount int64, s string) { - e.deleteRunes(int(byteOffset), int(runeCount)) - e.prepend(int(byteOffset), s) -} - -func (e *editBuffer) prepend(caret int, s string) { - if !utf8.ValidString(s) { - s = runes.ReplaceIllFormed().String(s) - } - - e.moveGap(caret, len(s)) - copy(e.text[caret:], s) - e.gapstart += len(s) - e.changed = e.changed || len(s) > 0 -} diff --git a/gio/widget/button.go b/gio/widget/button.go deleted file mode 100644 index 2a09ff0..0000000 --- a/gio/widget/button.go +++ /dev/null @@ -1,187 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - "time" - - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -// Clickable represents a clickable area. -type Clickable struct { - click gesture.Click - history []Press - - requestClicks int - pressedKey key.Name -} - -// Click represents a click. -type Click struct { - Modifiers key.Modifiers - NumClicks int -} - -// Press represents a past pointer press. -type Press struct { - // Position of the press. - Position image.Point - // Start is when the press began. - Start time.Time - // End is when the press was ended by a release or cancel. - // A zero End means it hasn't ended yet. - End time.Time - // Cancelled is true for cancelled presses. - Cancelled bool -} - -// Click executes a simple programmatic click. -func (b *Clickable) Click() { - b.requestClicks++ -} - -// Clicked calls Update and reports whether a click was registered. -func (b *Clickable) Clicked(gtx layout.Context) bool { - return b.clicked(b, gtx) -} - -func (b *Clickable) clicked(t event.Tag, gtx layout.Context) bool { - _, clicked := b.update(t, gtx) - return clicked -} - -// Hovered reports whether a pointer is over the element. -func (b *Clickable) Hovered() bool { - return b.click.Hovered() -} - -// Pressed reports whether a pointer is pressing the element. -func (b *Clickable) Pressed() bool { - return b.click.Pressed() -} - -// History is the past pointer presses useful for drawing markers. -// History is retained for a short duration (about a second). -func (b *Clickable) History() []Press { - return b.history -} - -// Layout and update the button state. -func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { - return b.layout(b, gtx, w) -} - -func (b *Clickable) layout(t event.Tag, gtx layout.Context, w layout.Widget) layout.Dimensions { - for { - _, ok := b.update(t, gtx) - if !ok { - break - } - } - m := op.Record(gtx.Ops) - dims := w(gtx) - c := m.Stop() - defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() - semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) - b.click.Add(gtx.Ops) - event.Op(gtx.Ops, t) - c.Add(gtx.Ops) - return dims -} - -// Update the button state by processing events, and return the next -// click, if any. -func (b *Clickable) Update(gtx layout.Context) (Click, bool) { - return b.update(b, gtx) -} - -func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) { - for len(b.history) > 0 { - c := b.history[0] - if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second { - break - } - n := copy(b.history, b.history[1:]) - b.history = b.history[:n] - } - if c := b.requestClicks; c > 0 { - b.requestClicks = 0 - return Click{ - NumClicks: c, - }, true - } - for { - e, ok := b.click.Update(gtx.Source) - if !ok { - break - } - switch e.Kind { - case gesture.KindClick: - if l := len(b.history); l > 0 { - b.history[l-1].End = gtx.Now - } - return Click{ - Modifiers: e.Modifiers, - NumClicks: e.NumClicks, - }, true - case gesture.KindCancel: - for i := range b.history { - b.history[i].Cancelled = true - if b.history[i].End.IsZero() { - b.history[i].End = gtx.Now - } - } - case gesture.KindPress: - b.history = append(b.history, Press{ - Position: e.Position, - Start: gtx.Now, - }) - } - } - for { - e, ok := gtx.Event( - key.FocusFilter{Target: t}, - key.Filter{Focus: t, Name: key.NameReturn}, - key.Filter{Focus: t, Name: key.NameSpace}, - ) - if !ok { - break - } - switch e := e.(type) { - case key.FocusEvent: - if e.Focus { - b.pressedKey = "" - } - case key.Event: - if !gtx.Focused(t) { - break - } - if e.Name != key.NameReturn && e.Name != key.NameSpace { - break - } - switch e.State { - case key.Press: - b.pressedKey = e.Name - case key.Release: - if b.pressedKey != e.Name { - break - } - // only register a key as a click if the key was pressed and released while this button was focused - b.pressedKey = "" - return Click{ - Modifiers: e.Modifiers, - NumClicks: 1, - }, true - } - } - } - return Click{}, false -} diff --git a/gio/widget/button_test.go b/gio/widget/button_test.go deleted file mode 100644 index cebf9aa..0000000 --- a/gio/widget/button_test.go +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget_test - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -func TestClickable(t *testing.T) { - var ( - r input.Router - b1 widget.Clickable - b2 widget.Clickable - ) - gtx := layout.Context{ - Ops: new(op.Ops), - Source: r.Source(), - } - layout := func() { - b1.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Dimensions{Size: image.Pt(100, 100)} - }) - // buttons are on top of each other but we only use focus and keyevents, so this is fine - b2.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Dimensions{Size: image.Pt(100, 100)} - }) - } - frame := func() { - gtx.Reset() - layout() - r.Frame(gtx.Ops) - } - gtx.Execute(key.FocusCmd{Tag: &b1}) - frame() - if !gtx.Focused(&b1) { - t.Error("button 1 did not gain focus") - } - if gtx.Focused(&b2) { - t.Error("button 2 should not have focus") - } - r.Queue( - key.Event{ - Name: key.NameReturn, - State: key.Press, - }, - key.Event{ - Name: key.NameReturn, - State: key.Release, - }, - ) - if !b1.Clicked(gtx) { - t.Error("button 1 did not get clicked when it got return press & release") - } - if b2.Clicked(gtx) { - t.Error("button 2 got clicked when it did not have focus") - } - r.Queue( - key.Event{ - Name: key.NameReturn, - State: key.Press, - }, - ) - if b1.Clicked(gtx) { - t.Error("button 1 got clicked, even if it only got return press") - } - frame() - gtx.Execute(key.FocusCmd{Tag: &b2}) - frame() - if gtx.Focused(&b1) { - t.Error("button 1 should not have focus") - } - if !gtx.Focused(&b2) { - t.Error("button 2 did not gain focus") - } - r.Queue( - key.Event{ - Name: key.NameReturn, - State: key.Release, - }, - ) - if b1.Clicked(gtx) { - t.Error("button 1 got clicked, even if it had lost focus") - } - if b2.Clicked(gtx) { - t.Error("button 2 should not have been clicked, as it only got return release") - } -} diff --git a/gio/widget/decorations.go b/gio/widget/decorations.go deleted file mode 100644 index 15bcf85..0000000 --- a/gio/widget/decorations.go +++ /dev/null @@ -1,63 +0,0 @@ -package widget - -import ( - "fmt" - "math/bits" - - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -// Decorations handles the states of window decorations. -type Decorations struct { - // Maximized controls the look and behaviour of the maximize - // button. It is the user's responsibility to set Maximized - // according to the window state reported through [app.ConfigEvent]. - Maximized bool - clicks map[int]*Clickable -} - -// LayoutMove lays out the widget that makes a window movable. -func (d *Decorations) LayoutMove(gtx layout.Context, w layout.Widget) layout.Dimensions { - dims := w(gtx) - defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop() - system.ActionInputOp(system.ActionMove).Add(gtx.Ops) - return dims -} - -// Clickable returns the clickable for the given single action. -func (d *Decorations) Clickable(action system.Action) *Clickable { - if bits.OnesCount(uint(action)) != 1 { - panic(fmt.Errorf("not a single action")) - } - idx := bits.TrailingZeros(uint(action)) - click, found := d.clicks[idx] - if !found { - click = new(Clickable) - if d.clicks == nil { - d.clicks = make(map[int]*Clickable) - } - d.clicks[idx] = click - } - return click -} - -// Update the state and return the set of actions activated by the user. -func (d *Decorations) Update(gtx layout.Context) system.Action { - var actions system.Action - for idx, clk := range d.clicks { - if !clk.Clicked(gtx) { - continue - } - action := system.Action(1 << idx) - switch { - case action == system.ActionMaximize && d.Maximized: - action = system.ActionUnmaximize - case action == system.ActionUnmaximize && !d.Maximized: - action = system.ActionMaximize - } - actions |= action - } - return actions -} diff --git a/gio/widget/dnd.go b/gio/widget/dnd.go deleted file mode 100644 index f78e468..0000000 --- a/gio/widget/dnd.go +++ /dev/null @@ -1,92 +0,0 @@ -package widget - -import ( - "io" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -// Draggable makes a widget draggable. -type Draggable struct { - // Type contains the MIME type and matches transfer.SourceOp. - Type string - - drag gesture.Drag - click f32.Point - pos f32.Point -} - -func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dimensions { - if !gtx.Enabled() { - return w(gtx) - } - dims := w(gtx) - - stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops) - d.drag.Add(gtx.Ops) - event.Op(gtx.Ops, d) - stack.Pop() - - if drag != nil && d.drag.Pressed() { - rec := op.Record(gtx.Ops) - op.Offset(d.pos.Round()).Add(gtx.Ops) - drag(gtx) - op.Defer(gtx.Ops, rec.Stop()) - } - - return dims -} - -// Dragging returns whether d is being dragged. -func (d *Draggable) Dragging() bool { - return d.drag.Dragging() -} - -// Update the draggable and returns the MIME type for which the Draggable was -// requested to offer data, if any -func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) { - pos := d.pos - for { - ev, ok := d.drag.Update(gtx.Metric, gtx.Source, gesture.Both) - if !ok { - break - } - switch ev.Kind { - case pointer.Press: - d.click = ev.Position - pos = f32.Point{} - case pointer.Drag, pointer.Release: - pos = ev.Position.Sub(d.click) - } - } - d.pos = pos - - for { - e, ok := gtx.Event(transfer.SourceFilter{Target: d, Type: d.Type}) - if !ok { - break - } - if e, ok := e.(transfer.RequestEvent); ok { - return e.Type, true - } - } - return "", false -} - -// Offer the data ready for a drop. Must be called after being Requested. -// The mime must be one in the requested list. -func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) { - gtx.Execute(transfer.OfferCmd{Tag: d, Type: mime, Data: data}) -} - -// Pos returns the drag position relative to its initial click position. -func (d *Draggable) Pos() f32.Point { - return d.pos -} diff --git a/gio/widget/dnd_test.go b/gio/widget/dnd_test.go deleted file mode 100644 index b52da5c..0000000 --- a/gio/widget/dnd_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package widget - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -func TestDraggable(t *testing.T) { - var r input.Router - gtx := layout.Context{ - Constraints: layout.Exact(image.Pt(100, 100)), - Source: r.Source(), - Ops: new(op.Ops), - } - - drag := &Draggable{ - Type: "file", - } - tgt := new(int) - defer pointer.PassOp{}.Push(gtx.Ops).Pop() - dims := drag.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.Dimensions{Size: gtx.Constraints.Min} - }, nil) - stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops) - event.Op(gtx.Ops, tgt) - stack.Pop() - - drag.Update(gtx) - r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) - r.Frame(gtx.Ops) - r.Queue( - pointer.Event{ - Position: f32.Pt(10, 10), - Kind: pointer.Press, - }, - pointer.Event{ - Position: f32.Pt(20, 10), - Kind: pointer.Move, - }, - pointer.Event{ - Position: f32.Pt(20, 10), - Kind: pointer.Release, - }, - ) - ofr := &offer{data: "hello"} - drag.Update(gtx) - r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) - drag.Offer(gtx, "file", ofr) - - e, ok := r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) - if !ok { - t.Fatalf("expected event") - } - ev := e.(transfer.DataEvent) - if got, want := ev.Type, "file"; got != want { - t.Errorf("expected %v; got %v", got, want) - } - if ofr.closed { - t.Error("offer closed prematurely") - } - e, ok = r.Event(transfer.TargetFilter{Target: tgt, Type: drag.Type}) - if !ok { - t.Fatalf("expected event") - } - if _, ok := e.(transfer.CancelEvent); !ok { - t.Fatalf("expected transfer.CancelEvent event") - } - r.Frame(gtx.Ops) - if !ofr.closed { - t.Error("offer was not closed") - } -} - -// offer satisfies io.ReadCloser for use in data transfers. -type offer struct { - data string - closed bool -} - -func (*offer) Read([]byte) (int, error) { return 0, nil } - -func (o *offer) Close() error { - o.closed = true - return nil -} diff --git a/gio/widget/doc.go b/gio/widget/doc.go deleted file mode 100644 index 97274b8..0000000 --- a/gio/widget/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package widget implements state tracking and event handling of -// common user interface controls. To draw widgets, use a theme -// packages such as package [gioui.org/widget/material]. -package widget diff --git a/gio/widget/editor.go b/gio/widget/editor.go deleted file mode 100644 index 334e991..0000000 --- a/gio/widget/editor.go +++ /dev/null @@ -1,1116 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "bufio" - "image" - "io" - "math" - "strings" - "time" - "unicode" - "unicode/utf8" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/clipboard" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// Editor implements an editable and scrollable text area. -type Editor struct { - // text manages the text buffer and provides shaping and cursor positioning - // services. - text textView - // Alignment controls the alignment of text within the editor. - Alignment text.Alignment - // LineHeight determines the gap between baselines of text. If zero, a sensible - // default will be used. - LineHeight unit.Sp - // LineHeightScale is multiplied by LineHeight to determine the final gap - // between baselines. If zero, a sensible default will be used. - LineHeightScale float32 - // SingleLine force the text to stay on a single line. - // SingleLine also sets the scrolling direction to - // horizontal. - SingleLine bool - // ReadOnly controls whether the contents of the editor can be altered by - // user interaction. If set to true, the editor will allow selecting text - // and copying it interactively, but not modifying it. - ReadOnly bool - // Submit enabled translation of carriage return keys to SubmitEvents. - // If not enabled, carriage returns are inserted as newlines in the text. - Submit bool - // Mask replaces the visual display of each rune in the contents with the given rune. - // Newline characters are not masked. When non-zero, the unmasked contents - // are accessed by Len, Text, and SetText. - Mask rune - // InputHint specifies the type of on-screen keyboard to be displayed. - InputHint key.InputHint - // MaxLen limits the editor content to a maximum length. Zero means no limit. - MaxLen int - // Filter is the list of characters allowed in the Editor. If Filter is empty, - // all characters are allowed. - Filter string - // WrapPolicy configures how displayed text will be broken into lines. - WrapPolicy text.WrapPolicy - - buffer *editBuffer - // scratch is a byte buffer that is reused to efficiently read portions of text - // from the textView. - scratch []byte - blinkStart time.Time - - // ime tracks the state relevant to input methods. - ime struct { - imeState - scratch []byte - } - - dragging bool - dragger gesture.Drag - scroller gesture.Scroll - scrollCaret bool - showCaret bool - - clicker gesture.Click - - // history contains undo history. - history []modification - // nextHistoryIdx is the index within the history of the next modification. This - // is only not len(history) immediately after undo operations occur. It is framed as the "next" value - // to make the zero value consistent. - nextHistoryIdx int - - pending []EditorEvent -} - -type offEntry struct { - runes int - bytes int -} - -type imeState struct { - selection struct { - rng key.Range - caret key.Caret - } - snippet key.Snippet - start, end int -} - -type maskReader struct { - // rr is the underlying reader. - rr io.RuneReader - maskBuf [utf8.UTFMax]byte - // mask is the utf-8 encoded mask rune. - mask []byte - // overflow contains excess mask bytes left over after the last Read call. - overflow []byte -} - -type selectionAction int - -const ( - selectionExtend selectionAction = iota - selectionClear -) - -func (m *maskReader) Reset(r io.Reader, mr rune) { - m.rr = bufio.NewReader(r) - n := utf8.EncodeRune(m.maskBuf[:], mr) - m.mask = m.maskBuf[:n] -} - -// Read reads from the underlying reader and replaces every -// rune with the mask rune. -func (m *maskReader) Read(b []byte) (n int, err error) { - for len(b) > 0 { - var replacement []byte - if len(m.overflow) > 0 { - replacement = m.overflow - } else { - var r rune - r, _, err = m.rr.ReadRune() - if err != nil { - break - } - if r == '\n' { - replacement = []byte{'\n'} - } else { - replacement = m.mask - } - } - nn := copy(b, replacement) - m.overflow = replacement[nn:] - n += nn - b = b[nn:] - } - return n, err -} - -type EditorEvent interface { - isEditorEvent() -} - -// A ChangeEvent is generated for every user change to the text. -type ChangeEvent struct{} - -// A SubmitEvent is generated when Submit is set -// and a carriage return key is pressed. -type SubmitEvent struct { - Text string -} - -// A SelectEvent is generated when the user selects some text, or changes the -// selection (e.g. with a shift-click), including if they remove the -// selection. The selected text is not part of the event, on the theory that -// it could be a relatively expensive operation (for a large editor), most -// applications won't actually care about it, and those that do can call -// Editor.SelectedText() (which can be empty). -type SelectEvent struct{} - -const ( - blinksPerSecond = 1 - maxBlinkDuration = 10 * time.Second -) - -func (e *Editor) processEvents(gtx layout.Context) (ev EditorEvent, ok bool) { - if len(e.pending) > 0 { - out := e.pending[0] - e.pending = e.pending[:copy(e.pending, e.pending[1:])] - return out, true - } - selStart, selEnd := e.Selection() - defer func() { - afterSelStart, afterSelEnd := e.Selection() - if selStart != afterSelStart || selEnd != afterSelEnd { - if ok { - e.pending = append(e.pending, SelectEvent{}) - } else { - ev = SelectEvent{} - ok = true - } - } - }() - - ev, ok = e.processPointer(gtx) - if ok { - return ev, ok - } - ev, ok = e.processKey(gtx) - if ok { - return ev, ok - } - return nil, false -} - -func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) { - sbounds := e.text.ScrollBounds() - var smin, smax int - var axis gesture.Axis - if e.SingleLine { - axis = gesture.Horizontal - smin, smax = sbounds.Min.X, sbounds.Max.X - } else { - axis = gesture.Vertical - smin, smax = sbounds.Min.Y, sbounds.Max.Y - } - var scrollX, scrollY pointer.ScrollRange - textDims := e.text.FullDimensions() - visibleDims := e.text.Dimensions() - if e.SingleLine { - scrollOffX := e.text.ScrollOff().X - scrollX.Min = min(-scrollOffX, 0) - scrollX.Max = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X)) - } else { - scrollOffY := e.text.ScrollOff().Y - scrollY.Min = -scrollOffY - scrollY.Max = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y)) - } - sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollX, scrollY) - var soff int - if e.SingleLine { - e.text.ScrollRel(sdist, 0) - soff = e.text.ScrollOff().X - } else { - e.text.ScrollRel(0, sdist) - soff = e.text.ScrollOff().Y - } - for { - evt, ok := e.clicker.Update(gtx.Source) - if !ok { - break - } - ev, ok := e.processPointerEvent(gtx, evt) - if ok { - return ev, ok - } - } - for { - evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both) - if !ok { - break - } - ev, ok := e.processPointerEvent(gtx, evt) - if ok { - return ev, ok - } - } - - if (sdist > 0 && soff >= smax) || (sdist < 0 && soff <= smin) { - e.scroller.Stop() - } - return nil, false -} - -func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (EditorEvent, bool) { - switch evt := ev.(type) { - case gesture.ClickEvent: - switch { - case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse, - evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse: - prevCaretPos, _ := e.text.Selection() - e.blinkStart = gtx.Now - e.text.MoveCoord(image.Point{ - X: int(math.Round(float64(evt.Position.X))), - Y: int(math.Round(float64(evt.Position.Y))), - }) - gtx.Execute(key.FocusCmd{Tag: e}) - if !e.ReadOnly { - gtx.Execute(key.SoftKeyboardCmd{Show: true}) - } - if e.scroller.State() != gesture.StateFlinging { - e.scrollCaret = true - } - - if evt.Modifiers == key.ModShift { - start, end := e.text.Selection() - // If they clicked closer to the end, then change the end to - // where the caret used to be (effectively swapping start & end). - if abs(end-start) < abs(start-prevCaretPos) { - e.text.SetCaret(start, prevCaretPos) - } - } else { - e.text.ClearSelection() - } - e.dragging = true - - // Process multi-clicks. - switch { - case evt.NumClicks == 2: - e.text.MoveWord(-1, selectionClear) - e.text.MoveWord(1, selectionExtend) - e.dragging = false - case evt.NumClicks >= 3: - e.text.MoveLineStart(selectionClear) - e.text.MoveLineEnd(selectionExtend) - e.dragging = false - } - } - case pointer.Event: - release := false - switch { - case evt.Kind == pointer.Release && evt.Source == pointer.Mouse: - release = true - fallthrough - case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse: - if e.dragging { - e.blinkStart = gtx.Now - e.text.MoveCoord(image.Point{ - X: int(math.Round(float64(evt.Position.X))), - Y: int(math.Round(float64(evt.Position.Y))), - }) - e.scrollCaret = true - - if release { - e.dragging = false - } - } - } - } - return nil, false -} - -func condFilter(pred bool, f key.Filter) event.Filter { - if pred { - return f - } else { - return nil - } -} - -func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) { - if e.text.Changed() { - return ChangeEvent{}, true - } - caret, _ := e.text.Selection() - atBeginning := caret == 0 - atEnd := caret == e.text.Len() - if gtx.Locale.Direction.Progression() != system.FromOrigin { - atEnd, atBeginning = atBeginning, atEnd - } - filters := []event.Filter{ - key.FocusFilter{Target: e}, - transfer.TargetFilter{Target: e, Type: "application/text"}, - key.Filter{Focus: e, Name: key.NameEnter, Optional: key.ModShift}, - key.Filter{Focus: e, Name: key.NameReturn, Optional: key.ModShift}, - - key.Filter{Focus: e, Name: "Z", Required: key.ModShortcut, Optional: key.ModShift}, - key.Filter{Focus: e, Name: "C", Required: key.ModShortcut}, - key.Filter{Focus: e, Name: "V", Required: key.ModShortcut}, - key.Filter{Focus: e, Name: "X", Required: key.ModShortcut}, - key.Filter{Focus: e, Name: "A", Required: key.ModShortcut}, - - key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift}, - - key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShortcut | key.ModShift}, - key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShortcut | key.ModShift}, - key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift}, - key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift}, - condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}), - condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}), - condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}), - condFilter(!atEnd, key.Filter{Focus: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}), - } - // adjust keeps track of runes dropped because of MaxLen. - var adjust int - for { - ke, ok := gtx.Event(filters...) - if !ok { - break - } - e.blinkStart = gtx.Now - switch ke := ke.(type) { - case key.FocusEvent: - // Reset IME state. - e.ime.imeState = imeState{} - if ke.Focus && !e.ReadOnly { - gtx.Execute(key.SoftKeyboardCmd{Show: true}) - } - case key.Event: - if !gtx.Focused(e) || ke.State != key.Press { - break - } - if !e.ReadOnly && e.Submit && (ke.Name == key.NameReturn || ke.Name == key.NameEnter) { - if !ke.Modifiers.Contain(key.ModShift) { - e.scratch = e.text.Text(e.scratch) - return SubmitEvent{ - Text: string(e.scratch), - }, true - } - } - e.scrollCaret = true - e.scroller.Stop() - ev, ok := e.command(gtx, ke) - if ok { - return ev, ok - } - case key.SnippetEvent: - e.updateSnippet(gtx, ke.Start, ke.End) - case key.EditEvent: - if e.ReadOnly { - break - } - e.scrollCaret = true - e.scroller.Stop() - s := ke.Text - moves := 0 - submit := false - switch { - case e.Submit: - if i := strings.IndexByte(s, '\n'); i != -1 { - submit = true - moves += len(s) - i - s = s[:i] - } - case e.SingleLine: - s = strings.ReplaceAll(s, "\n", " ") - } - moves += e.replace(ke.Range.Start, ke.Range.End, s, true) - adjust += utf8.RuneCountInString(ke.Text) - moves - // Reset caret xoff. - e.text.MoveCaret(0, 0) - if submit { - e.scratch = e.text.Text(e.scratch) - submitEvent := SubmitEvent{ - Text: string(e.scratch), - } - if e.text.Changed() { - e.pending = append(e.pending, submitEvent) - return ChangeEvent{}, true - } - return submitEvent, true - } - // Complete a paste event, initiated by Shortcut-V in Editor.command(). - case transfer.DataEvent: - e.scrollCaret = true - e.scroller.Stop() - content, err := io.ReadAll(ke.Open()) - if err == nil { - if e.Insert(string(content)) != 0 { - return ChangeEvent{}, true - } - } - case key.SelectionEvent: - e.scrollCaret = true - e.scroller.Stop() - ke.Start -= adjust - ke.End -= adjust - adjust = 0 - e.text.SetCaret(ke.Start, ke.End) - } - } - if e.text.Changed() { - return ChangeEvent{}, true - } - return nil, false -} - -func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) { - direction := 1 - if gtx.Locale.Direction.Progression() == system.TowardOrigin { - direction = -1 - } - moveByWord := k.Modifiers.Contain(key.ModShortcutAlt) - selAct := selectionClear - if k.Modifiers.Contain(key.ModShift) { - selAct = selectionExtend - } - if k.Modifiers.Contain(key.ModShortcut) { - switch k.Name { - // Initiate a paste operation, by requesting the clipboard contents; other - // half is in Editor.processKey() under clipboard.Event. - case "V": - if !e.ReadOnly { - gtx.Execute(clipboard.ReadCmd{Tag: e}) - } - // Copy or Cut selection -- ignored if nothing selected. - case "C", "X": - e.scratch = e.text.SelectedText(e.scratch) - if text := string(e.scratch); text != "" { - gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))}) - if k.Name == "X" && !e.ReadOnly { - if e.Delete(1) != 0 { - return ChangeEvent{}, true - } - } - } - // Select all - case "A": - e.text.SetCaret(0, e.text.Len()) - case "Z": - if !e.ReadOnly { - if k.Modifiers.Contain(key.ModShift) { - if ev, ok := e.redo(); ok { - return ev, ok - } - } else { - if ev, ok := e.undo(); ok { - return ev, ok - } - } - } - case key.NameHome: - e.text.MoveTextStart(selAct) - case key.NameEnd: - e.text.MoveTextEnd(selAct) - } - return nil, false - } - switch k.Name { - case key.NameReturn, key.NameEnter: - if !e.ReadOnly { - if e.Insert("\n") != 0 { - return ChangeEvent{}, true - } - } - case key.NameDeleteBackward: - if !e.ReadOnly { - if moveByWord { - if e.deleteWord(-1) != 0 { - return ChangeEvent{}, true - } - } else { - if e.Delete(-1) != 0 { - return ChangeEvent{}, true - } - } - } - case key.NameDeleteForward: - if !e.ReadOnly { - if moveByWord { - if e.deleteWord(1) != 0 { - return ChangeEvent{}, true - } - } else { - if e.Delete(1) != 0 { - return ChangeEvent{}, true - } - } - } - case key.NameUpArrow: - e.text.MoveLines(-1, selAct) - case key.NameDownArrow: - e.text.MoveLines(+1, selAct) - case key.NameLeftArrow: - if moveByWord { - e.text.MoveWord(-1*direction, selAct) - } else { - if selAct == selectionClear { - e.text.ClearSelection() - } - e.text.MoveCaret(-1*direction, -1*direction*int(selAct)) - } - case key.NameRightArrow: - if moveByWord { - e.text.MoveWord(1*direction, selAct) - } else { - if selAct == selectionClear { - e.text.ClearSelection() - } - e.text.MoveCaret(1*direction, int(selAct)*direction) - } - case key.NamePageUp: - e.text.MovePages(-1, selAct) - case key.NamePageDown: - e.text.MovePages(+1, selAct) - case key.NameHome: - e.text.MoveLineStart(selAct) - case key.NameEnd: - e.text.MoveLineEnd(selAct) - } - return nil, false -} - -// initBuffer should be invoked first in every exported function that accesses -// text state. It ensures that the underlying text widget is both ready to use -// and has its fields synced with the editor. -func (e *Editor) initBuffer() { - if e.buffer == nil { - e.buffer = new(editBuffer) - e.text.SetSource(e.buffer) - } - e.text.Alignment = e.Alignment - e.text.LineHeight = e.LineHeight - e.text.LineHeightScale = e.LineHeightScale - e.text.SingleLine = e.SingleLine - e.text.Mask = e.Mask - e.text.WrapPolicy = e.WrapPolicy - e.text.DisableSpaceTrim = true -} - -// Update the state of the editor in response to input events. Update consumes editor -// input events until there are no remaining events or an editor event is generated. -// To fully update the state of the editor, callers should call Update until it returns -// false. -func (e *Editor) Update(gtx layout.Context) (EditorEvent, bool) { - e.initBuffer() - event, ok := e.processEvents(gtx) - // Notify IME of selection if it changed. - newSel := e.ime.selection - start, end := e.text.Selection() - newSel.rng = key.Range{ - Start: start, - End: end, - } - caretPos, carAsc, carDesc := e.text.CaretInfo() - newSel.caret = key.Caret{ - Pos: layout.FPt(caretPos), - Ascent: float32(carAsc), - Descent: float32(carDesc), - } - if newSel != e.ime.selection { - e.ime.selection = newSel - gtx.Execute(key.SelectionCmd{Tag: e, Range: newSel.rng, Caret: newSel.caret}) - } - - e.updateSnippet(gtx, e.ime.start, e.ime.end) - return event, ok -} - -// Layout lays out the editor using the provided textMaterial as the paint material -// for the text glyphs+caret and the selectMaterial as the paint material for the -// selection rectangle. -func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions { - for { - _, ok := e.Update(gtx) - if !ok { - break - } - } - - e.text.Layout(gtx, lt, font, size) - return e.layout(gtx, textMaterial, selectMaterial) -} - -// updateSnippet queues a key.SnippetCmd if the snippet content or position -// have changed. off and len are in runes. -func (e *Editor) updateSnippet(gtx layout.Context, start, end int) { - if start > end { - start, end = end, start - } - length := e.text.Len() - if start > length { - start = length - } - if end > length { - end = length - } - e.ime.start = start - e.ime.end = end - startOff := e.text.ByteOffset(start) - endOff := e.text.ByteOffset(end) - n := endOff - startOff - if n > int64(len(e.ime.scratch)) { - e.ime.scratch = make([]byte, n) - } - scratch := e.ime.scratch[:n] - read, _ := e.text.ReadAt(scratch, startOff) - if read != len(scratch) { - panic("e.rr.Read truncated data") - } - newSnip := key.Snippet{ - Range: key.Range{ - Start: e.ime.start, - End: e.ime.end, - }, - Text: e.ime.snippet.Text, - } - if string(scratch) != newSnip.Text { - newSnip.Text = string(scratch) - } - if newSnip == e.ime.snippet { - return - } - e.ime.snippet = newSnip - gtx.Execute(key.SnippetCmd{Tag: e, Snippet: newSnip}) -} - -func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.CallOp) layout.Dimensions { - // Adjust scrolling for new viewport and layout. - e.text.ScrollRel(0, 0) - - if e.scrollCaret { - e.scrollCaret = false - e.text.ScrollToCaret() - } - visibleDims := e.text.Dimensions() - - defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop() - pointer.CursorText.Add(gtx.Ops) - event.Op(gtx.Ops, e) - key.InputHintOp{Tag: e, Hint: e.InputHint}.Add(gtx.Ops) - - e.scroller.Add(gtx.Ops) - - e.clicker.Add(gtx.Ops) - e.dragger.Add(gtx.Ops) - e.showCaret = false - if gtx.Focused(e) { - now := gtx.Now - dt := now.Sub(e.blinkStart) - blinking := dt < maxBlinkDuration - const timePerBlink = time.Second / blinksPerSecond - nextBlink := now.Add(timePerBlink/2 - dt%(timePerBlink/2)) - if blinking { - gtx.Execute(op.InvalidateCmd{At: nextBlink}) - } - e.showCaret = !blinking || dt%timePerBlink < timePerBlink/2 - } - semantic.Editor.Add(gtx.Ops) - if e.Len() > 0 { - e.paintSelection(gtx, selectMaterial) - e.paintText(gtx, textMaterial) - } - if gtx.Enabled() { - e.paintCaret(gtx, textMaterial) - } - return visibleDims -} - -// paintSelection paints the contrasting background for selected text using the provided -// material to set the painting material for the selection. -func (e *Editor) paintSelection(gtx layout.Context, material op.CallOp) { - e.initBuffer() - if !gtx.Focused(e) { - return - } - e.text.PaintSelection(gtx, material) -} - -// paintText paints the text glyphs using the provided material to set the fill of the -// glyphs. -func (e *Editor) paintText(gtx layout.Context, material op.CallOp) { - e.initBuffer() - e.text.PaintText(gtx, material) -} - -// paintCaret paints the text glyphs using the provided material to set the fill material -// of the caret rectangle. -func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) { - e.initBuffer() - if !e.showCaret || e.ReadOnly { - return - } - e.text.PaintCaret(gtx, material) -} - -// Len is the length of the editor contents, in runes. -func (e *Editor) Len() int { - e.initBuffer() - return e.text.Len() -} - -// Text returns the contents of the editor. -func (e *Editor) Text() string { - e.initBuffer() - e.scratch = e.text.Text(e.scratch) - return string(e.scratch) -} - -func (e *Editor) SetText(s string) { - e.initBuffer() - if e.SingleLine { - s = strings.ReplaceAll(s, "\n", " ") - } - e.replace(0, e.text.Len(), s, true) - // Reset xoff and move the caret to the beginning. - e.SetCaret(0, 0) -} - -// CaretPos returns the line & column numbers of the caret. -func (e *Editor) CaretPos() (line, col int) { - e.initBuffer() - return e.text.CaretPos() -} - -// CaretCoords returns the coordinates of the caret, relative to the -// editor itself. -func (e *Editor) CaretCoords() f32.Point { - e.initBuffer() - return e.text.CaretCoords() -} - -// Delete runes from the caret position. The sign of the argument specifies the -// direction to delete: positive is forward, negative is backward. -// -// If there is a selection, it is deleted and counts as a single grapheme -// cluster. -func (e *Editor) Delete(graphemeClusters int) (deletedRunes int) { - e.initBuffer() - if graphemeClusters == 0 { - return 0 - } - - start, end := e.text.Selection() - if start != end { - graphemeClusters -= sign(graphemeClusters) - } - - // Move caret by the target quantity of clusters. - e.text.MoveCaret(0, graphemeClusters) - // Get the new rune offsets of the selection. - start, end = e.text.Selection() - e.replace(start, end, "", true) - // Reset xoff. - e.text.MoveCaret(0, 0) - e.ClearSelection() - return end - start -} - -func (e *Editor) Insert(s string) (insertedRunes int) { - e.initBuffer() - if e.SingleLine { - s = strings.ReplaceAll(s, "\n", " ") - } - start, end := e.text.Selection() - moves := e.replace(start, end, s, true) - if end < start { - start = end - } - // Reset xoff. - e.text.MoveCaret(0, 0) - e.SetCaret(start+moves, start+moves) - e.scrollCaret = true - return moves -} - -// modification represents a change to the contents of the editor buffer. -// It contains the necessary information to both apply the change and -// reverse it, and is useful for implementing undo/redo. -type modification struct { - // StartRune is the inclusive index of the first rune - // modified. - StartRune int - // ApplyContent is the data inserted at StartRune to - // apply this operation. It overwrites len([]rune(ReverseContent)) runes. - ApplyContent string - // ReverseContent is the data inserted at StartRune to - // apply this operation. It overwrites len([]rune(ApplyContent)) runes. - ReverseContent string -} - -// undo applies the modification at e.history[e.historyIdx] and decrements -// e.historyIdx. -func (e *Editor) undo() (EditorEvent, bool) { - e.initBuffer() - if len(e.history) < 1 || e.nextHistoryIdx == 0 { - return nil, false - } - mod := e.history[e.nextHistoryIdx-1] - replaceEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent) - e.replace(mod.StartRune, replaceEnd, mod.ReverseContent, false) - caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent) - e.SetCaret(caretEnd, mod.StartRune) - e.nextHistoryIdx-- - return ChangeEvent{}, true -} - -// redo applies the modification at e.history[e.historyIdx] and increments -// e.historyIdx. -func (e *Editor) redo() (EditorEvent, bool) { - e.initBuffer() - if len(e.history) < 1 || e.nextHistoryIdx == len(e.history) { - return nil, false - } - mod := e.history[e.nextHistoryIdx] - end := mod.StartRune + utf8.RuneCountInString(mod.ReverseContent) - e.replace(mod.StartRune, end, mod.ApplyContent, false) - caretEnd := mod.StartRune + utf8.RuneCountInString(mod.ApplyContent) - e.SetCaret(caretEnd, mod.StartRune) - e.nextHistoryIdx++ - return ChangeEvent{}, true -} - -// replace the text between start and end with s. Indices are in runes. -// It returns the number of runes inserted. -// addHistory controls whether this modification is recorded in the undo -// history. replace can modify text in positions unrelated to the cursor -// position. -func (e *Editor) replace(start, end int, s string, addHistory bool) int { - length := e.text.Len() - if start > end { - start, end = end, start - } - start = min(start, length) - end = min(end, length) - replaceSize := end - start - el := e.Len() - var sc int - idx := 0 - for idx < len(s) { - if e.MaxLen > 0 && el-replaceSize+sc >= e.MaxLen { - s = s[:idx] - break - } - _, n := utf8.DecodeRuneInString(s[idx:]) - if e.Filter != "" && !strings.Contains(e.Filter, s[idx:idx+n]) { - s = s[:idx] + s[idx+n:] - continue - } - idx += n - sc++ - } - - if addHistory { - deleted := make([]rune, 0, replaceSize) - readPos := e.text.ByteOffset(start) - for range replaceSize { - ru, s, _ := e.text.ReadRuneAt(int64(readPos)) - readPos += int64(s) - deleted = append(deleted, ru) - } - if e.nextHistoryIdx < len(e.history) { - e.history = e.history[:e.nextHistoryIdx] - } - e.history = append(e.history, modification{ - StartRune: start, - ApplyContent: s, - ReverseContent: string(deleted), - }) - e.nextHistoryIdx++ - } - - sc = e.text.Replace(start, end, s) - newEnd := start + sc - adjust := func(pos int) int { - switch { - case newEnd < pos && pos <= end: - pos = newEnd - case end < pos: - diff := newEnd - end - pos = pos + diff - } - return pos - } - e.ime.start = adjust(e.ime.start) - e.ime.end = adjust(e.ime.end) - return sc -} - -// MoveCaret moves the caret (aka selection start) and the selection end -// relative to their current positions. Positive distances moves forward, -// negative distances moves backward. Distances are in grapheme clusters, -// which closely match what users perceive as "characters" even when the -// characters are multiple code points long. -func (e *Editor) MoveCaret(startDelta, endDelta int) { - e.initBuffer() - e.text.MoveCaret(startDelta, endDelta) -} - -// deleteWord deletes the next word(s) in the specified direction. -// Unlike moveWord, deleteWord treats whitespace as a word itself. -// Positive is forward, negative is backward. -// Absolute values greater than one will delete that many words. -// The selection counts as a single word. -func (e *Editor) deleteWord(distance int) (deletedRunes int) { - if distance == 0 { - return - } - - start, end := e.text.Selection() - if start != end { - deletedRunes = e.Delete(1) - distance -= sign(distance) - } - if distance == 0 { - return deletedRunes - } - - // split the distance information into constituent parts to be - // used independently. - words, direction := distance, 1 - if distance < 0 { - words, direction = distance*-1, -1 - } - caret, _ := e.text.Selection() - // atEnd if offset is at or beyond either side of the buffer. - atEnd := func(runes int) bool { - idx := caret + runes*direction - return idx <= 0 || idx >= e.Len() - } - // next returns the appropriate rune given the direction and offset in runes). - next := func(runes int) rune { - idx := caret + runes*direction - if idx < 0 { - idx = 0 - } else if idx > e.Len() { - idx = e.Len() - } - off := e.text.ByteOffset(idx) - var r rune - if direction < 0 { - r, _, _ = e.text.ReadRuneBefore(int64(off)) - } else { - r, _, _ = e.text.ReadRuneAt(int64(off)) - } - return r - } - runes := 1 - for range words { - r := next(runes) - wantSpace := unicode.IsSpace(r) - for r := next(runes); unicode.IsSpace(r) == wantSpace && !atEnd(runes); r = next(runes) { - runes += 1 - } - } - deletedRunes += e.Delete(runes * direction) - return deletedRunes -} - -// SelectionLen returns the length of the selection, in runes; it is -// equivalent to utf8.RuneCountInString(e.SelectedText()). -func (e *Editor) SelectionLen() int { - e.initBuffer() - return e.text.SelectionLen() -} - -// Selection returns the start and end of the selection, as rune offsets. -// start can be > end. -func (e *Editor) Selection() (start, end int) { - e.initBuffer() - return e.text.Selection() -} - -// SetCaret moves the caret to start, and sets the selection end to end. start -// and end are in runes, and represent offsets into the editor text. -func (e *Editor) SetCaret(start, end int) { - e.initBuffer() - e.text.SetCaret(start, end) - e.scrollCaret = true - e.scroller.Stop() -} - -// SelectedText returns the currently selected text (if any) from the editor. -func (e *Editor) SelectedText() string { - e.initBuffer() - e.scratch = e.text.SelectedText(e.scratch) - return string(e.scratch) -} - -// ClearSelection clears the selection, by setting the selection end equal to -// the selection start. -func (e *Editor) ClearSelection() { - e.initBuffer() - e.text.ClearSelection() -} - -// WriteTo implements io.WriterTo. -func (e *Editor) WriteTo(w io.Writer) (int64, error) { - e.initBuffer() - return e.text.WriteTo(w) -} - -// Seek implements io.Seeker. -func (e *Editor) Seek(offset int64, whence int) (int64, error) { - e.initBuffer() - return e.text.Seek(offset, whence) -} - -// Read implements io.Reader. -func (e *Editor) Read(p []byte) (int, error) { - e.initBuffer() - return e.text.Read(p) -} - -// Regions returns visible regions covering the rune range [start,end). -func (e *Editor) Regions(start, end int, regions []Region) []Region { - e.initBuffer() - return e.text.Regions(start, end, regions) -} - -func abs(n int) int { - if n < 0 { - return -n - } - return n -} - -func sign(n int) int { - switch { - case n < 0: - return -1 - case n > 0: - return 1 - default: - return 0 - } -} - -func (s ChangeEvent) isEditorEvent() {} -func (s SubmitEvent) isEditorEvent() {} -func (s SelectEvent) isEditorEvent() {} diff --git a/gio/widget/editor_test.go b/gio/widget/editor_test.go deleted file mode 100644 index db30912..0000000 --- a/gio/widget/editor_test.go +++ /dev/null @@ -1,1285 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "bytes" - "fmt" - "image" - "io" - "math/rand" - "reflect" - "testing" - "testing/quick" - "time" - "unicode/utf8" - - nsareg "eliasnaur.com/font/noto/sans/arabic/regular" - "eliasnaur.com/font/roboto/robotoregular" - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/font/gofont" - "github.com/p9c/p9/pkg/gel/gio/font/opentype" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -var english = system.Locale{ - Language: "EN", - Direction: system.LTR, -} - -// TestEditorHistory ensures that undo and redo behave correctly. -func TestEditorHistory(t *testing.T) { - e := new(Editor) - // Insert some multi-byte unicode text. - e.SetText("안П你 hello 안П你") - assertContents(t, e, "안П你 hello 안П你", 0, 0) - // Overwrite all of the text with the empty string. - e.SetCaret(0, len([]rune("안П你 hello 안П你"))) - e.Insert("") - assertContents(t, e, "", 0, 0) - // Ensure that undoing the overwrite succeeds. - e.undo() - assertContents(t, e, "안П你 hello 안П你", 13, 0) - // Ensure that redoing the overwrite succeeds. - e.redo() - assertContents(t, e, "", 0, 0) - // Insert some smaller text. - e.Insert("안П你 hello") - assertContents(t, e, "안П你 hello", 9, 9) - // Replace a region in the middle of the text. - e.SetCaret(1, 5) - e.Insert("П") - assertContents(t, e, "안Пello", 2, 2) - // Replace a second region in the middle. - e.SetCaret(3, 4) - e.Insert("П") - assertContents(t, e, "안ПeПlo", 4, 4) - // Ensure both operations undo successfully. - e.undo() - assertContents(t, e, "안Пello", 4, 3) - e.undo() - assertContents(t, e, "안П你 hello", 5, 1) - // Make a new modification. - e.Insert("Something New") - // Ensure that redo history is discarded now that - // we've diverged from the linear editing history. - // This redo() call should do nothing. - text := e.Text() - start, end := e.Selection() - e.redo() - assertContents(t, e, text, start, end) -} - -func assertContents(t *testing.T, e *Editor, contents string, selectionStart, selectionEnd int) { - t.Helper() - actualContents := e.Text() - if actualContents != contents { - t.Errorf("expected editor to contain %s, got %s", contents, actualContents) - } - actualStart, actualEnd := e.Selection() - if actualStart != selectionStart { - t.Errorf("expected selection start to be %d, got %d", selectionStart, actualStart) - } - if actualEnd != selectionEnd { - t.Errorf("expected selection end to be %d, got %d", selectionEnd, actualEnd) - } -} - -// TestEditorReadOnly ensures that mouse and keyboard interactions with readonly -// editors do nothing but manipulate the text selection. -func TestEditorReadOnly(t *testing.T) { - r := new(input.Router) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{ - Max: image.Pt(100, 100), - }, - Locale: english, - Source: r.Source(), - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fontSize := unit.Sp(10) - font := font.Font{} - e := new(Editor) - e.ReadOnly = true - e.SetText("The quick brown fox jumps over the lazy dog. We just need a few lines of text in the editor so that it can adequately test a few different modes of selection. The quick brown fox jumps over the lazy dog. We just need a few lines of text in the editor so that it can adequately test a few different modes of selection.") - cStart, cEnd := e.Selection() - if cStart != cEnd { - t.Errorf("unexpected initial caret positions") - } - gtx.Execute(key.FocusCmd{Tag: e}) - layoutEditor := func() layout.Dimensions { - return e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - } - layoutEditor() - r.Frame(gtx.Ops) - gtx.Ops.Reset() - layoutEditor() - r.Frame(gtx.Ops) - gtx.Ops.Reset() - layoutEditor() - r.Frame(gtx.Ops) - - // Select everything. - gtx.Ops.Reset() - r.Queue(key.Event{Name: "A", Modifiers: key.ModShortcut}) - layoutEditor() - textContent := e.Text() - cStart2, cEnd2 := e.Selection() - if cStart2 > cEnd2 { - cStart2, cEnd2 = cEnd2, cStart2 - } - if cEnd2 != e.Len() { - t.Errorf("expected selection to contain %d runes, got %d", e.Len(), cEnd2) - } - if cStart2 != 0 { - t.Errorf("expected selection to start at rune 0, got %d", cStart2) - } - - // Type some new characters. - gtx.Ops.Reset() - r.Queue(key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}) - e.Update(gtx) - textContent2 := e.Text() - if textContent2 != textContent { - t.Errorf("readonly editor modified by key.EditEvent") - } - - // Try to delete selection. - gtx.Ops.Reset() - r.Queue(key.Event{Name: key.NameDeleteBackward}) - dims := layoutEditor() - textContent2 = e.Text() - if textContent2 != textContent { - t.Errorf("readonly editor modified by delete key.Event") - } - - // Click and drag from the middle of the first line - // to the center. - gtx.Ops.Reset() - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Buttons: pointer.ButtonPrimary, - Position: f32.Pt(float32(dims.Size.X)*.5, 5), - }, - pointer.Event{ - Kind: pointer.Move, - Buttons: pointer.ButtonPrimary, - Position: layout.FPt(dims.Size).Mul(.5), - }, - pointer.Event{ - Kind: pointer.Release, - Buttons: pointer.ButtonPrimary, - Position: layout.FPt(dims.Size).Mul(.5), - }, - ) - e.Update(gtx) - cStart3, cEnd3 := e.Selection() - if cStart3 == cStart2 || cEnd3 == cEnd2 { - t.Errorf("expected mouse interaction to change selection.") - } -} - -func TestEditorConfigurations(t *testing.T) { - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(300, 300)), - Locale: english, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fontSize := unit.Sp(10) - font := font.Font{} - sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog" - runes := len([]rune(sentence)) - - // Ensure that both ends of the text are reachable in all permutations - // of settings that influence layout. - for _, singleLine := range []bool{true, false} { - for _, alignment := range []text.Alignment{text.Start, text.Middle, text.End} { - for _, zeroMin := range []bool{true, false} { - t.Run(fmt.Sprintf("SingleLine: %v Alignment: %v ZeroMinConstraint: %v", singleLine, alignment, zeroMin), func(t *testing.T) { - defer func() { - if err := recover(); err != nil { - t.Error(err) - } - }() - if zeroMin { - gtx.Constraints.Min = image.Point{} - } else { - gtx.Constraints.Min = gtx.Constraints.Max - } - e := new(Editor) - e.SingleLine = singleLine - e.Alignment = alignment - e.SetText(sentence) - e.SetCaret(0, 0) - dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - if dims.Size.X < gtx.Constraints.Min.X || dims.Size.Y < gtx.Constraints.Min.Y { - t.Errorf("expected min size %#+v, got %#+v", gtx.Constraints.Min, dims.Size) - } - coords := e.CaretCoords() - if halfway := float32(gtx.Constraints.Min.X) * .5; !singleLine && alignment == text.Middle && !zeroMin && coords.X != halfway { - t.Errorf("expected caret X to be %f, got %f", halfway, coords.X) - } - e.SetCaret(runes, runes) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - coords = e.CaretCoords() - if int(coords.X) > gtx.Constraints.Max.X || int(coords.Y) > gtx.Constraints.Max.Y { - t.Errorf("caret coordinates %v exceed constraints %v", coords, gtx.Constraints.Max) - } - }) - } - } - } -} - -func TestEditor(t *testing.T) { - e := new(Editor) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Locale: english, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fontSize := unit.Sp(10) - font := font.Font{} - - // Regression test for bad in-cluster rune offset math. - e.SetText("æbc") - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - e.text.MoveLineEnd(selectionClear) - assertCaret(t, e, 0, 3, len("æbc")) - - textSample := "æbc\naøå••" - e.SetCaret(0, 0) // shouldn't panic - assertCaret(t, e, 0, 0, 0) - e.SetText(textSample) - if got, exp := e.Len(), utf8.RuneCountInString(e.Text()); got != exp { - t.Errorf("got length %d, expected %d", got, exp) - } - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - assertCaret(t, e, 0, 0, 0) - e.text.MoveLineEnd(selectionClear) - assertCaret(t, e, 0, 3, len("æbc")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 0, len("æbc\n")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 3, len("æbc")) - e.text.MoveLines(+1, selectionClear) - assertCaret(t, e, 1, 4, len("æbc\naøå•")) - e.text.MoveLineEnd(selectionClear) - assertCaret(t, e, 1, 5, len("æbc\naøå••")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 5, len("æbc\naøå••")) - e.text.MoveLines(3, selectionClear) - - e.SetCaret(0, 0) - assertCaret(t, e, 0, 0, 0) - e.SetCaret(utf8.RuneCountInString("æ"), utf8.RuneCountInString("æ")) - assertCaret(t, e, 0, 1, 2) - e.SetCaret(utf8.RuneCountInString("æbc\naøå•"), utf8.RuneCountInString("æbc\naøå•")) - assertCaret(t, e, 1, 4, len("æbc\naøå•")) - - // Ensure that password masking does not affect caret behavior - e.MoveCaret(-3, -3) - assertCaret(t, e, 1, 1, len("æbc\na")) - e.text.Mask = '*' - e.Update(gtx) - assertCaret(t, e, 1, 1, len("æbc\na")) - e.MoveCaret(-3, -3) - assertCaret(t, e, 0, 2, len("æb")) - // Test that moveLine applies x offsets from previous moves. - e.SetText("long line\nshort") - e.SetCaret(0, 0) - e.text.MoveLineEnd(selectionClear) - e.text.MoveLines(+1, selectionClear) - e.text.MoveLines(-1, selectionClear) - assertCaret(t, e, 0, utf8.RuneCountInString("long line"), len("long line")) -} - -var arabic = system.Locale{ - Language: "AR", - Direction: system.RTL, -} - -var arabicCollection = func() []font.FontFace { - parsed, _ := opentype.Parse(nsareg.TTF) - return []font.FontFace{{Font: font.Font{}, Face: parsed}} -}() - -func TestEditorRTL(t *testing.T) { - e := new(Editor) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Locale: arabic, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(arabicCollection)) - fontSize := unit.Sp(10) - font := font.Font{} - - e.SetCaret(0, 0) // shouldn't panic - assertCaret(t, e, 0, 0, 0) - - // Set the text to a single RTL word. The caret should start at 0 column - // zero, but this is the first column on the right. - e.SetText("الحب") - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - assertCaret(t, e, 0, 0, 0) - e.MoveCaret(+1, +1) - assertCaret(t, e, 0, 1, len("ا")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 0, 2, len("ال")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 0, 3, len("الح")) - // Move to the "end" of the line. This moves to the left edge of the line. - e.text.MoveLineEnd(selectionClear) - assertCaret(t, e, 0, 4, len("الحب")) - - sentence := "الحب سماء لا\nتمط غير الأحلام" - e.SetText(sentence) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - assertCaret(t, e, 0, 0, 0) - e.text.MoveLineEnd(selectionClear) - assertCaret(t, e, 0, 12, len("الحب سماء لا")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 0, len("الحب سماء لا\n")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 1, len("الحب سماء لا\nت")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 1, 0, len("الحب سماء لا\n")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 12, len("الحب سماء لا")) - e.text.MoveLines(+1, selectionClear) - assertCaret(t, e, 1, 14, len("الحب سماء لا\nتمط غير الأحلا")) - e.text.MoveLineEnd(selectionClear) - assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام")) - e.text.MoveLines(3, selectionClear) - assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام")) - e.SetCaret(utf8.RuneCountInString(sentence), 0) - assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام")) - if selection := e.SelectedText(); selection != sentence { - t.Errorf("expected selection %s, got %s", sentence, selection) - } - - e.SetCaret(0, 0) - assertCaret(t, e, 0, 0, 0) - e.SetCaret(utf8.RuneCountInString("ا"), utf8.RuneCountInString("ا")) - assertCaret(t, e, 0, 1, len("ا")) - e.SetCaret(utf8.RuneCountInString("الحب سماء لا\nتمط غ"), utf8.RuneCountInString("الحب سماء لا\nتمط غ")) - assertCaret(t, e, 1, 5, len("الحب سماء لا\nتمط غ")) -} - -func TestEditorLigature(t *testing.T) { - e := new(Editor) - e.WrapPolicy = text.WrapWords - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Locale: english, - } - face, err := opentype.Parse(robotoregular.TTF) - if err != nil { - t.Skipf("failed parsing test font: %v", err) - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{ - { - Font: font.Font{ - Typeface: "Roboto", - }, - Face: face, - }, - })) - fontSize := unit.Sp(10) - font := font.Font{} - - /* - In this font, the following rune sequences form ligatures: - - - ffi - - ffl - - fi - - fl - */ - - e.SetCaret(0, 0) // shouldn't panic - assertCaret(t, e, 0, 0, 0) - e.SetText("fl") // just a ligature - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - e.text.MoveLineEnd(selectionClear) - assertCaret(t, e, 0, 2, len("fl")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 1, len("f")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 0, 0) - e.MoveCaret(+2, +2) - assertCaret(t, e, 0, 2, len("fl")) - e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1 - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - assertCaret(t, e, 0, 0, 0) - e.text.MoveLineEnd(selectionClear) - assertCaret(t, e, 0, 10, len("ffaffl•ffi")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 0, len("ffaffl•ffi\n")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 1, len("ffaffl•ffi\n•")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 2, len("ffaffl•ffi\n•f")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 3, len("ffaffl•ffi\n•ff")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 4, len("ffaffl•ffi\n•ffl")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 5, len("ffaffl•ffi\n•fflf")) - e.MoveCaret(+1, +1) - assertCaret(t, e, 1, 6, len("ffaffl•ffi\n•fflfi")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 1, 5, len("ffaffl•ffi\n•fflf")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 1, 4, len("ffaffl•ffi\n•ffl")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 1, 3, len("ffaffl•ffi\n•ff")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 1, 2, len("ffaffl•ffi\n•f")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 1, 1, len("ffaffl•ffi\n•")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 1, 0, len("ffaffl•ffi\n")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 10, len("ffaffl•ffi")) - e.MoveCaret(-2, -2) - assertCaret(t, e, 0, 8, len("ffaffl•f")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 7, len("ffaffl•")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 6, len("ffaffl")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 5, len("ffaff")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 4, len("ffaf")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 3, len("ffa")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 2, len("ff")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 1, len("f")) - e.MoveCaret(-1, -1) - assertCaret(t, e, 0, 0, 0) - gtx.Constraints = layout.Exact(image.Pt(50, 50)) - e.SetText("fflffl fflffl fflffl fflffl") // Many ligatures broken across lines. - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - // Ensure that all runes in the final cluster of a line are properly - // decoded when moving to the end of the line. This is a regression test. - e.text.MoveLineEnd(selectionClear) - // The first line was broken by line wrapping, not a newline character, and has a trailing - // whitespace. However, we should never be able to reach the "other side" of such a trailing - // whitespace glyph. - assertCaret(t, e, 0, 13, len("fflffl fflffl")) - e.text.MoveLines(1, selectionClear) - assertCaret(t, e, 1, 13, len("fflffl fflffl fflffl fflffl")) - e.text.MoveLines(-1, selectionClear) - assertCaret(t, e, 0, 13, len("fflffl fflffl")) - - // Absurdly narrow constraints to force each ligature onto its own line. - gtx.Constraints = layout.Exact(image.Pt(10, 10)) - e.SetText("ffl ffl") // Two ligatures on separate lines. - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - assertCaret(t, e, 0, 0, 0) - e.MoveCaret(1, 1) // Move the caret into the first ligature. - assertCaret(t, e, 0, 1, len("f")) - e.MoveCaret(4, 4) // Move the caret several positions. - assertCaret(t, e, 1, 1, len("ffl f")) -} - -func TestEditorDimensions(t *testing.T) { - e := new(Editor) - r := new(input.Router) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{Max: image.Pt(100, 100)}, - Source: r.Source(), - Locale: english, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fontSize := unit.Sp(10) - font := font.Font{} - gtx.Execute(key.FocusCmd{Tag: e}) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - r.Queue(key.EditEvent{Text: "A"}) - dims := e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - if dims.Size.X < 5 { - t.Errorf("EditEvent was not reflected in Editor width") - } -} - -// assertCaret asserts that the editor caret is at a particular line -// and column, and that the byte position matches as well. -func assertCaret(t *testing.T, e *Editor, line, col, bytes int) { - t.Helper() - gotLine, gotCol := e.CaretPos() - if gotLine != line || gotCol != col { - t.Errorf("caret at (%d, %d), expected (%d, %d)", gotLine, gotCol, line, col) - } - caretBytes := e.text.runeOffset(e.text.caret.start) - if bytes != caretBytes { - t.Errorf("caret at buffer position %d, expected %d", caretBytes, bytes) - } - // Ensure that SelectedText() does not panic no matter what the - // editor's state is. - _ = e.SelectedText() -} - -type editMutation int - -const ( - setText editMutation = iota - moveRune - moveLine - movePage - moveTextStart - moveTextEnd - moveLineStart - moveLineEnd - moveCoord - moveWord - deleteWord - moveLast // Mark end; never generated. -) - -func TestEditorCaretConsistency(t *testing.T) { - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Locale: english, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fontSize := unit.Sp(10) - font := font.Font{} - for _, a := range []text.Alignment{text.Start, text.Middle, text.End} { - e := &Editor{} - e.Alignment = a - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - - consistent := func() error { - t.Helper() - gotLine, gotCol := e.CaretPos() - gotCoords := e.CaretCoords() - // Blow away index to re-compute position from scratch. - e.text.invalidate() - want := e.text.closestToRune(e.text.caret.start) - wantCoords := f32.Pt(float32(want.x)/64, float32(want.y)) - if want.lineCol.line != gotLine || int(want.lineCol.col) != gotCol || gotCoords != wantCoords { - return fmt.Errorf("caret (%d,%d) pos %s, want (%d,%d) pos %s", - gotLine, gotCol, gotCoords, want.lineCol.line, want.lineCol.col, wantCoords) - } - return nil - } - if err := consistent(); err != nil { - t.Errorf("initial editor inconsistency (alignment %s): %v", a, err) - } - - move := func(mutation editMutation, str string, distance int8, x, y uint16) bool { - switch mutation { - case setText: - e.SetText(str) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - case moveRune: - e.MoveCaret(int(distance), int(distance)) - case moveLine: - e.text.MoveLines(int(distance), selectionClear) - case movePage: - e.text.MovePages(int(distance), selectionClear) - case moveLineStart: - e.text.MoveLineStart(selectionClear) - case moveLineEnd: - e.text.MoveLineEnd(selectionClear) - case moveTextStart: - e.text.MoveTextStart(selectionClear) - case moveTextEnd: - e.text.MoveTextEnd(selectionClear) - case moveCoord: - e.text.MoveCoord(image.Pt(int(x), int(y))) - case moveWord: - e.text.MoveWord(int(distance), selectionClear) - case deleteWord: - e.deleteWord(int(distance)) - default: - return false - } - if err := consistent(); err != nil { - t.Error(err) - return false - } - return true - } - if err := quick.Check(move, nil); err != nil { - t.Errorf("editor inconsistency (alignment %s): %v", a, err) - } - } -} - -func TestEditorMoveWord(t *testing.T) { - type Test struct { - Text string - Start int - Skip int - Want int - } - tests := []Test{ - {"", 0, 0, 0}, - {"", 0, -1, 0}, - {"", 0, 1, 0}, - {"hello", 0, -1, 0}, - {"hello", 0, 1, 5}, - {"hello world", 3, 1, 5}, - {"hello world", 3, -1, 0}, - {"hello world", 8, -1, 6}, - {"hello world", 8, 1, 11}, - {"hello world", 3, 1, 5}, - {"hello world", 3, 2, 14}, - {"hello world", 8, 1, 14}, - {"hello world", 8, -1, 0}, - {"hello brave new world", 0, 3, 15}, - } - setup := func(t string) *Editor { - e := new(Editor) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Locale: english, - } - e.SetText(t) - e.Update(gtx) - return e - } - for ii, tt := range tests { - e := setup(tt.Text) - e.MoveCaret(tt.Start, tt.Start) - e.text.MoveWord(tt.Skip, selectionClear) - caretBytes := e.text.runeOffset(e.text.caret.start) - if caretBytes != tt.Want { - t.Fatalf("[%d] moveWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want) - } - } -} - -func TestEditorInsert(t *testing.T) { - type Test struct { - Text string - Start int - Selection int - Insertion string - - Result string - } - tests := []Test{ - // Nothing inserted - {"", 0, 0, "", ""}, - {"", 0, -1, "", ""}, - {"", 0, 1, "", ""}, - {"", 0, -2, "", ""}, - {"", 0, 2, "", ""}, - {"world", 0, 0, "", "world"}, - {"world", 0, -1, "", "world"}, - {"world", 0, 1, "", "orld"}, - {"world", 2, 0, "", "world"}, - {"world", 2, -1, "", "wrld"}, - {"world", 2, 1, "", "wold"}, - {"world", 5, 0, "", "world"}, - {"world", 5, -1, "", "worl"}, - {"world", 5, 1, "", "world"}, - // One rune inserted - {"", 0, 0, "_", "_"}, - {"", 0, -1, "_", "_"}, - {"", 0, 1, "_", "_"}, - {"", 0, -2, "_", "_"}, - {"", 0, 2, "_", "_"}, - {"world", 0, 0, "_", "_world"}, - {"world", 0, -1, "_", "_world"}, - {"world", 0, 1, "_", "_orld"}, - {"world", 2, 0, "_", "wo_rld"}, - {"world", 2, -1, "_", "w_rld"}, - {"world", 2, 1, "_", "wo_ld"}, - {"world", 5, 0, "_", "world_"}, - {"world", 5, -1, "_", "worl_"}, - {"world", 5, 1, "_", "world_"}, - // More runes inserted - {"", 0, 0, "-3-", "-3-"}, - {"", 0, -1, "-3-", "-3-"}, - {"", 0, 1, "-3-", "-3-"}, - {"", 0, -2, "-3-", "-3-"}, - {"", 0, 2, "-3-", "-3-"}, - {"world", 0, 0, "-3-", "-3-world"}, - {"world", 0, -1, "-3-", "-3-world"}, - {"world", 0, 1, "-3-", "-3-orld"}, - {"world", 2, 0, "-3-", "wo-3-rld"}, - {"world", 2, -1, "-3-", "w-3-rld"}, - {"world", 2, 1, "-3-", "wo-3-ld"}, - {"world", 5, 0, "-3-", "world-3-"}, - {"world", 5, -1, "-3-", "worl-3-"}, - {"world", 5, 1, "-3-", "world-3-"}, - // Runes with length > 1 inserted - {"", 0, 0, "éêè", "éêè"}, - {"", 0, -1, "éêè", "éêè"}, - {"", 0, 1, "éêè", "éêè"}, - {"", 0, -2, "éêè", "éêè"}, - {"", 0, 2, "éêè", "éêè"}, - {"world", 0, 0, "éêè", "éêèworld"}, - {"world", 0, -1, "éêè", "éêèworld"}, - {"world", 0, 1, "éêè", "éêèorld"}, - {"world", 2, 0, "éêè", "woéêèrld"}, - {"world", 2, -1, "éêè", "wéêèrld"}, - {"world", 2, 1, "éêè", "woéêèld"}, - {"world", 5, 0, "éêè", "worldéêè"}, - {"world", 5, -1, "éêè", "worléêè"}, - {"world", 5, 1, "éêè", "worldéêè"}, - // Runes with length > 1 deleted from selection - {"élançé", 0, 1, "", "lançé"}, - {"élançé", 0, 1, "-3-", "-3-lançé"}, - {"élançé", 3, 2, "-3-", "éla-3-é"}, - {"élançé", 3, 3, "-3-", "éla-3-"}, - {"élançé", 3, 10, "-3-", "éla-3-"}, - {"élançé", 5, -1, "-3-", "élan-3-é"}, - {"élançé", 6, -1, "-3-", "élanç-3-"}, - {"élançé", 6, -3, "-3-", "éla-3-"}, - } - setup := func(t string) *Editor { - e := new(Editor) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Locale: english, - } - e.SetText(t) - e.Update(gtx) - return e - } - for ii, tt := range tests { - e := setup(tt.Text) - e.MoveCaret(tt.Start, tt.Start) - e.MoveCaret(0, tt.Selection) - e.Insert(tt.Insertion) - if e.Text() != tt.Result { - t.Fatalf("[%d] Insert: invalid result: got %q, want %q", ii, e.Text(), tt.Result) - } - } -} - -func TestEditorDeleteWord(t *testing.T) { - type Test struct { - Text string - Start int - Selection int - Delete int - - Want int - Result string - } - tests := []Test{ - // No text selected - {"", 0, 0, 0, 0, ""}, - {"", 0, 0, -1, 0, ""}, - {"", 0, 0, 1, 0, ""}, - {"", 0, 0, -2, 0, ""}, - {"", 0, 0, 2, 0, ""}, - {"hello", 0, 0, -1, 0, "hello"}, - {"hello", 0, 0, 1, 0, ""}, - - // Document (imho) incorrect behavior w.r.t. deleting spaces following - // words. - {"hello world", 0, 0, 1, 0, " world"}, // Should be "world", if you ask me. - {"hello world", 0, 0, 2, 0, "world"}, // Should be "". - {"hello ", 0, 0, 1, 0, " "}, // Should be "". - {"hello world", 11, 0, -1, 6, "hello "}, // Should be "hello". - {"hello world", 11, 0, -2, 5, "hello"}, // Should be "". - {"hello ", 6, 0, -1, 0, ""}, // Correct result. - - {"hello world", 3, 0, 1, 3, "hel world"}, - {"hello world", 3, 0, -1, 0, "lo world"}, - {"hello world", 8, 0, -1, 6, "hello rld"}, - {"hello world", 8, 0, 1, 8, "hello wo"}, - {"hello world", 3, 0, 1, 3, "hel world"}, - {"hello world", 3, 0, 2, 3, "helworld"}, - {"hello world", 8, 0, 1, 8, "hello "}, - {"hello world", 8, 0, -1, 5, "hello world"}, - {"hello brave new world", 0, 0, 3, 0, " new world"}, - {"helléèçàô world", 3, 0, 1, 3, "hel world"}, // unicode char with length > 1 in deleted part - // Add selected text. - // - // Several permutations must be tested: - // - select from the left or right - // - Delete + or - - // - abs(Delete) == 1 or > 1 - // - // "brave |" selected; caret at | - {"hello there brave new world", 12, 6, 1, 12, "hello there new world"}, // #16 - {"hello there brave new world", 12, 6, 2, 12, "hello there world"}, // The two spaces after "there" are actually suboptimal, if you ask me. See also above cases. - {"hello there brave new world", 12, 6, -1, 12, "hello there new world"}, - {"hello there brave new world", 12, 6, -2, 6, "hello new world"}, - {"hello there b®âve new world", 12, 6, 1, 12, "hello there new world"}, // unicode chars with length > 1 in selection - {"hello there b®âve new world", 12, 6, 2, 12, "hello there world"}, // ditto - {"hello there b®âve new world", 12, 6, -1, 12, "hello there new world"}, // ditto - {"hello there b®âve new world", 12, 6, -2, 6, "hello new world"}, // ditto - // "|brave " selected - {"hello there brave new world", 18, -6, 1, 12, "hello there new world"}, // #20 - {"hello there brave new world", 18, -6, 2, 12, "hello there world"}, // ditto - {"hello there brave new world", 18, -6, -1, 12, "hello there new world"}, - {"hello there brave new world", 18, -6, -2, 6, "hello new world"}, - {"hello there b®âve new world", 18, -6, 1, 12, "hello there new world"}, // unicode chars with length > 1 in selection - // Random edge cases - {"hello there brave new world", 12, 6, 99, 12, "hello there "}, - {"hello there brave new world", 18, -6, -99, 0, "new world"}, - } - setup := func(t string) *Editor { - e := new(Editor) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Locale: english, - } - e.SetText(t) - e.Update(gtx) - return e - } - for ii, tt := range tests { - e := setup(tt.Text) - e.MoveCaret(tt.Start, tt.Start) - e.MoveCaret(0, tt.Selection) - e.deleteWord(tt.Delete) - caretBytes := e.text.runeOffset(e.text.caret.start) - if caretBytes != tt.Want { - t.Fatalf("[%d] deleteWord: bad caret position: got %d, want %d", ii, caretBytes, tt.Want) - } - if e.Text() != tt.Result { - t.Fatalf("[%d] deleteWord: invalid result: got %q, want %q", ii, e.Text(), tt.Result) - } - } -} - -func TestEditorNoLayout(t *testing.T) { - var e Editor - e.SetText("hi!\n") - e.MoveCaret(1, 1) -} - -// Generate generates a value of itself, for testing/quick. -func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value { - t := editMutation(rand.Intn(int(moveLast))) - return reflect.ValueOf(t) -} - -// TestEditorSelect tests the selection code. It lays out an editor with several -// lines in it, selects some text, verifies the selection, resizes the editor -// to make it much narrower (which makes the lines in the editor reflow), and -// then verifies that the updated (col, line) positions of the selected text -// are where we expect. -func TestEditorSelectReflow(t *testing.T) { - e := new(Editor) - e.SetText(`a 2 4 6 8 a -b 2 4 6 8 b -c 2 4 6 8 c -d 2 4 6 8 d -e 2 4 6 8 e -f 2 4 6 8 f -g 2 4 6 8 g -`) - - r := new(input.Router) - gtx := layout.Context{ - Ops: new(op.Ops), - Locale: english, - Source: r.Source(), - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - font := font.Font{} - fontSize := unit.Sp(10) - - var tim time.Duration - selected := func(start, end int) string { - gtx.Execute(key.FocusCmd{Tag: e}) - // Layout once with no events; populate e.lines. - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - - r.Frame(gtx.Ops) - gtx.Source = r.Source() - // Build the selection events - startPos := e.text.closestToRune(start) - endPos := e.text.closestToRune(end) - r.Queue( - pointer.Event{ - Buttons: pointer.ButtonPrimary, - Kind: pointer.Press, - Source: pointer.Mouse, - Time: tim, - Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)), - }, - pointer.Event{ - Kind: pointer.Release, - Source: pointer.Mouse, - Time: tim, - Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)), - }, - ) - tim += time.Second // Avoid multi-clicks. - - for { - _, ok := e.Update(gtx) // throw away any events from this layout - if !ok { - break - } - } - return e.SelectedText() - } - type screenPos image.Point - logicalPosMatch := func(t *testing.T, n int, label string, expected screenPos, actual combinedPos) { - t.Helper() - if actual.lineCol.line != expected.Y || actual.lineCol.col != expected.X { - t.Errorf("Test %d: Expected %s %#v; got %#v", - n, label, - expected, actual) - } - } - - type testCase struct { - // input text offsets - start, end int - - // expected selected text - selection string - // expected line/col positions of selection after resize - startPos, endPos screenPos - } - - for n, tst := range []testCase{ - {0, 1, "a", screenPos{}, screenPos{Y: 0, X: 1}}, - {0, 4, "a 2 ", screenPos{}, screenPos{Y: 0, X: 4}}, - {0, 11, "a 2 4 6 8 a", screenPos{}, screenPos{Y: 1, X: 3}}, - {6, 10, "6 8 ", screenPos{Y: 0, X: 6}, screenPos{Y: 1, X: 2}}, - {41, 66, " 6 8 d\ne 2 4 6 8 e\nf 2 4 ", screenPos{Y: 6, X: 5}, screenPos{Y: 10, X: 6}}, - } { - gtx.Constraints = layout.Exact(image.Pt(100, 100)) - if got := selected(tst.start, tst.end); got != tst.selection { - t.Errorf("Test %d pt1: Expected %q, got %q", n, tst.selection, got) - continue - } - - // Constrain the editor to roughly 6 columns wide and redraw - gtx.Constraints = layout.Exact(image.Pt(36, 36)) - // Keep existing selection - gtx = gtx.Disabled() - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - - caretStart := e.text.closestToRune(e.text.caret.start) - caretEnd := e.text.closestToRune(e.text.caret.end) - logicalPosMatch(t, n, "start", tst.startPos, caretEnd) - logicalPosMatch(t, n, "end", tst.endPos, caretStart) - } -} - -func TestEditorSelectShortcuts(t *testing.T) { - tFont := font.Font{} - tFontSize := unit.Sp(10) - tShaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - tEditor := &Editor{ - SingleLine: false, - ReadOnly: true, - } - lines := "abc abc abc\ndef def def\nghi ghi ghi" - tEditor.SetText(lines) - type testCase struct { - // Initial text selection. - startPos, endPos int - // Keyboard shortcut to execute. - keyEvent key.Event - // Expected text selection. - selection string - } - - pos1, pos2 := 14, 21 - for n, tst := range []testCase{ - {pos1, pos2, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines}, - {pos2, pos1, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines}, - {pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "def def d"}, - {pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "ef"}, - {pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "de"}, - {pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "f def def"}, - {pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\ndef def d"}, - {pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "ef\nghi ghi ghi"}, - {pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\nde"}, - {pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "f def def\nghi ghi ghi"}, - } { - tRouter := new(input.Router) - gtx := layout.Context{ - Ops: new(op.Ops), - Locale: english, - Constraints: layout.Exact(image.Pt(100, 100)), - Source: tRouter.Source(), - } - gtx.Execute(key.FocusCmd{Tag: tEditor}) - tEditor.Layout(gtx, tShaper, tFont, tFontSize, op.CallOp{}, op.CallOp{}) - - tEditor.SetCaret(tst.startPos, tst.endPos) - if cStart, cEnd := tEditor.Selection(); cStart != tst.startPos || cEnd != tst.endPos { - t.Errorf("TestEditorSelect %d: initial selection", n) - } - tRouter.Queue(tst.keyEvent) - tEditor.Update(gtx) - if got := tEditor.SelectedText(); got != tst.selection { - t.Errorf("TestEditorSelect %d: Expected %q, got %q", n, tst.selection, got) - } - } -} - -// Verify that an existing selection is dismissed when you press arrow keys. -func TestSelectMove(t *testing.T) { - e := new(Editor) - e.SetText(`0123456789`) - - r := new(input.Router) - gtx := layout.Context{ - Ops: new(op.Ops), - Locale: english, - Source: r.Source(), - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - font := font.Font{} - fontSize := unit.Sp(10) - - // Layout once to populate e.lines and get focus. - gtx.Execute(key.FocusCmd{Tag: e}) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - // Set up selecton so the Editor key handler filters for all 4 directional keys. - e.SetCaret(3, 6) - gtx.Ops.Reset() - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - gtx.Ops.Reset() - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - - for _, keyName := range []key.Name{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} { - // Select 345 - e.SetCaret(3, 6) - if expected, got := "345", e.SelectedText(); expected != got { - t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) - } - - // Press the key - r.Queue(key.Event{State: key.Press, Name: keyName}) - gtx.Ops.Reset() - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - - if expected, got := "", e.SelectedText(); expected != got { - t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) - } - } -} - -func TestEditor_Read(t *testing.T) { - s := "hello world" - buf := make([]byte, len(s)) - e := new(Editor) - e.SetText(s) - - _, err := e.Seek(0, io.SeekStart) - if err != nil { - t.Error(err) - } - n, err := io.ReadFull(e, buf) - if err != nil { - t.Error(err) - } - if got, want := n, len(s); got != want { - t.Errorf("got %d; want %d", got, want) - } - if got, want := string(buf), s; got != want { - t.Errorf("got %q; want %q", got, want) - } -} - -func TestEditor_WriteTo(t *testing.T) { - s := "hello world" - var buf bytes.Buffer - e := new(Editor) - e.SetText(s) - - n, err := io.Copy(&buf, e) - if err != nil { - t.Error(err) - } - if got, want := int(n), len(s); got != want { - t.Errorf("got %d; want %d", got, want) - } - if got, want := buf.String(), s; got != want { - t.Errorf("got %q; want %q", got, want) - } -} - -func TestEditor_MaxLen(t *testing.T) { - e := new(Editor) - - e.MaxLen = 8 - e.SetText("123456789") - if got, want := e.Text(), "12345678"; got != want { - t.Errorf("editor failed to cap SetText") - } - - e.SetText("2345678") - r := new(input.Router) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Source: r.Source(), - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fontSize := unit.Sp(10) - font := font.Font{} - gtx.Execute(key.FocusCmd{Tag: e}) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - r.Queue( - key.EditEvent{Range: key.Range{Start: 0, End: 2}, Text: "1234"}, - key.SelectionEvent{Start: 4, End: 4}, - ) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - - if got, want := e.Text(), "12345678"; got != want { - t.Errorf("editor failed to cap EditEvent") - } - if start, end := e.Selection(); start != 3 || end != 3 { - t.Errorf("editor failed to adjust SelectionEvent") - } -} - -func TestEditor_Filter(t *testing.T) { - e := new(Editor) - - e.Filter = "123456789" - e.SetText("abcde1234") - if got, want := e.Text(), "1234"; got != want { - t.Errorf("editor failed to filter SetText") - } - - e.SetText("2345678") - r := new(input.Router) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Source: r.Source(), - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fontSize := unit.Sp(10) - font := font.Font{} - gtx.Execute(key.FocusCmd{Tag: e}) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - r.Queue( - key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1"}, - key.SelectionEvent{Start: 4, End: 4}, - ) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - - if got, want := e.Text(), "12345678"; got != want { - t.Errorf("editor failed to filter EditEvent") - } - if start, end := e.Selection(); start != 2 || end != 2 { - t.Errorf("editor failed to adjust SelectionEvent") - } -} - -func TestEditor_Submit(t *testing.T) { - e := new(Editor) - e.Submit = true - - r := new(input.Router) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Source: r.Source(), - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fontSize := unit.Sp(10) - font := font.Font{} - gtx.Execute(key.FocusCmd{Tag: e}) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - r.Queue( - key.EditEvent{Range: key.Range{Start: 0, End: 0}, Text: "ab1\n"}, - ) - - got := []EditorEvent{} - for { - ev, ok := e.Update(gtx) - if !ok { - break - } - got = append(got, ev) - } - if got, want := e.Text(), "ab1"; got != want { - t.Errorf("editor failed to filter newline") - } - want := []EditorEvent{ - ChangeEvent{}, - SubmitEvent{Text: e.Text()}, - } - if !reflect.DeepEqual(want, got) { - t.Errorf("editor failed to register submit") - } -} - -func TestNoFilterAllocs(t *testing.T) { - b := testing.Benchmark(func(b *testing.B) { - r := new(input.Router) - e := new(Editor) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{ - Max: image.Pt(100, 100), - }, - Locale: english, - Source: r.Source(), - } - b.ReportAllocs() - b.ResetTimer() - for b.Loop() { - e.Update(gtx) - } - }) - if allocs := b.AllocsPerOp(); allocs != 0 { - t.Fatalf("expected 0 AllocsPerOp, got %d", allocs) - } -} - -// textWidth is a text helper for building simple selection events. -// It assumes single-run lines, which isn't safe with non-test text -// data. -func textWidth(e *Editor, lineNum, colStart, colEnd int) float32 { - start := e.text.closestToLineCol(lineNum, colStart) - end := e.text.closestToLineCol(lineNum, colEnd) - delta := start.x - end.x - if delta < 0 { - delta = -delta - } - return float32(delta.Round()) -} - -// testBaseline returns the y coordinate of the baseline for the -// given line number. -func textBaseline(e *Editor, lineNum int) float32 { - start := e.text.closestToLineCol(lineNum, 0) - return float32(start.y) -} diff --git a/gio/widget/enum.go b/gio/widget/enum.go deleted file mode 100644 index 91bf8ee..0000000 --- a/gio/widget/enum.go +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" -) - -type Enum struct { - Value string - hovered string - hovering bool - - focus string - focused bool - - keys []*enumKey -} - -type enumKey struct { - key string - click gesture.Click - tag struct{} -} - -func (e *Enum) index(k string) *enumKey { - for _, v := range e.keys { - if v.key == k { - return v - } - } - return nil -} - -// Update the state and report whether Value has changed by user interaction. -func (e *Enum) Update(gtx layout.Context) bool { - if !gtx.Enabled() { - e.focused = false - } - e.hovering = false - changed := false - for _, state := range e.keys { - for { - ev, ok := state.click.Update(gtx.Source) - if !ok { - break - } - switch ev.Kind { - case gesture.KindPress: - if ev.Source == pointer.Mouse { - gtx.Execute(key.FocusCmd{Tag: &state.tag}) - } - case gesture.KindClick: - if state.key != e.Value { - e.Value = state.key - changed = true - } - } - } - for { - ev, ok := gtx.Event( - key.FocusFilter{Target: &state.tag}, - key.Filter{Focus: &state.tag, Name: key.NameReturn}, - key.Filter{Focus: &state.tag, Name: key.NameSpace}, - ) - if !ok { - break - } - switch ev := ev.(type) { - case key.FocusEvent: - if ev.Focus { - e.focused = true - e.focus = state.key - } else if state.key == e.focus { - e.focused = false - } - case key.Event: - if ev.State != key.Release { - break - } - if ev.Name != key.NameReturn && ev.Name != key.NameSpace { - break - } - if state.key != e.Value { - e.Value = state.key - changed = true - } - } - } - if state.click.Hovered() { - e.hovered = state.key - e.hovering = true - } - } - - return changed -} - -// Hovered returns the key that is highlighted, or false if none are. -func (e *Enum) Hovered() (string, bool) { - return e.hovered, e.hovering -} - -// Focused reports the focused key, or false if no key is focused. -func (e *Enum) Focused() (string, bool) { - return e.focus, e.focused -} - -// Layout adds the event handler for the key k. -func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layout.Dimensions { - e.Update(gtx) - m := op.Record(gtx.Ops) - dims := content(gtx) - c := m.Stop() - defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop() - - state := e.index(k) - if state == nil { - state = &enumKey{ - key: k, - } - e.keys = append(e.keys, state) - } - clk := &state.click - clk.Add(gtx.Ops) - event.Op(gtx.Ops, &state.tag) - semantic.SelectedOp(k == e.Value).Add(gtx.Ops) - semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops) - c.Add(gtx.Ops) - - return dims -} diff --git a/gio/widget/example_test.go b/gio/widget/example_test.go deleted file mode 100644 index 770d36b..0000000 --- a/gio/widget/example_test.go +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget_test - -import ( - "fmt" - "image" - "io" - "strings" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/transfer" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -func ExampleClickable_passthrough() { - // When laying out clickable widgets on top of each other, - // pointer events can be passed down for the underlying - // widgets to pick them up. - var button1, button2 widget.Clickable - var r input.Router - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Source: r.Source(), - } - - // widget lays out two buttons on top of each other. - widget := func() { - content := func(gtx layout.Context) layout.Dimensions { return layout.Dimensions{Size: gtx.Constraints.Min} } - button1.Layout(gtx, content) - // button2 completely covers button1, but pass-through allows pointer - // events to pass through to button1. - defer pointer.PassOp{}.Push(gtx.Ops).Pop() - button2.Layout(gtx, content) - } - - // The first layout and call to Frame declare the Clickable handlers - // to the input router, so the following pointer events are propagated. - widget() - r.Frame(gtx.Ops) - // Simulate one click on the buttons by sending a Press and Release event. - r.Queue( - pointer.Event{ - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - Kind: pointer.Press, - Position: f32.Pt(50, 50), - }, - pointer.Event{ - Source: pointer.Mouse, - Buttons: pointer.ButtonPrimary, - Kind: pointer.Release, - Position: f32.Pt(50, 50), - }, - ) - - if button1.Clicked(gtx) { - fmt.Println("button1 clicked!") - } - if button2.Clicked(gtx) { - fmt.Println("button2 clicked!") - } - - // Output: - // button1 clicked! - // button2 clicked! -} - -func ExampleDraggable_Layout() { - var r input.Router - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - Source: r.Source(), - } - // mime is the type used to match drag and drop operations. - // It could be left empty in this example. - const mime = "MyMime" - drag := &widget.Draggable{Type: mime} - var drop int - // widget lays out the drag and drop handlers and processes - // the transfer events. - widget := func() { - // Setup the draggable widget. - w := func(gtx layout.Context) layout.Dimensions { - sz := image.Pt(10, 10) // drag area - return layout.Dimensions{Size: sz} - } - drag.Layout(gtx, w, w) - // drag must respond with an Offer event when requested. - // Use the drag method for this. - if m, ok := drag.Update(gtx); ok { - drag.Offer(gtx, m, io.NopCloser(strings.NewReader("hello world"))) - } - - // Setup the area for drops. - ds := clip.Rect{ - Min: image.Pt(20, 20), - Max: image.Pt(40, 40), - }.Push(gtx.Ops) - event.Op(gtx.Ops, &drop) - ds.Pop() - - // Check for the received data. - for { - ev, ok := gtx.Event(transfer.TargetFilter{Target: &drop, Type: mime}) - if !ok { - break - } - switch e := ev.(type) { - case transfer.DataEvent: - data := e.Open() - defer data.Close() - content, _ := io.ReadAll(data) - fmt.Println(string(content)) - } - } - } - // Register and lay out the widget. - widget() - r.Frame(gtx.Ops) - - // Send drag and drop gesture events. - r.Queue( - pointer.Event{ - Kind: pointer.Press, - Position: f32.Pt(5, 5), // in the drag area - }, - pointer.Event{ - Kind: pointer.Move, - Position: f32.Pt(5, 5), // in the drop area - }, - pointer.Event{ - Kind: pointer.Release, - Position: f32.Pt(30, 30), // in the drop area - }, - ) - // Let the widget process the events. - widget() - r.Frame(gtx.Ops) - - // Process the transfer.DataEvent. - widget() - - // Output: - // hello world -} diff --git a/gio/widget/fit.go b/gio/widget/fit.go deleted file mode 100644 index 365a7e3..0000000 --- a/gio/widget/fit.go +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/layout" -) - -// Fit scales a widget to fit and clip to the constraints. -type Fit uint8 - -const ( - // Unscaled does not alter the scale of a widget. - Unscaled Fit = iota - // Contain scales widget as large as possible without cropping - // and it preserves aspect-ratio. - Contain - // Cover scales the widget to cover the constraint area and - // preserves aspect-ratio. - Cover - // ScaleDown scales the widget smaller without cropping, - // when it exceeds the constraint area. - // It preserves aspect-ratio. - ScaleDown - // Fill stretches the widget to the constraints and does not - // preserve aspect-ratio. - Fill -) - -// scale computes the new dimensions and transformation required to fit dims to cs, given the position. -func (fit Fit) scale(cs layout.Constraints, pos layout.Direction, dims layout.Dimensions) (layout.Dimensions, f32.Affine2D) { - widgetSize := dims.Size - - if fit == Unscaled || dims.Size.X == 0 || dims.Size.Y == 0 { - dims.Size = cs.Constrain(dims.Size) - - offset := pos.Position(widgetSize, dims.Size) - dims.Baseline += offset.Y - return dims, f32.AffineId().Offset(layout.FPt(offset)) - } - - scale := f32.Point{ - X: float32(cs.Max.X) / float32(dims.Size.X), - Y: float32(cs.Max.Y) / float32(dims.Size.Y), - } - - switch fit { - case Contain: - if scale.Y < scale.X { - scale.X = scale.Y - } else { - scale.Y = scale.X - } - case Cover: - if scale.Y > scale.X { - scale.X = scale.Y - } else { - scale.Y = scale.X - } - case ScaleDown: - if scale.Y < scale.X { - scale.X = scale.Y - } else { - scale.Y = scale.X - } - - // The widget would need to be scaled up, no change needed. - if scale.X >= 1 { - dims.Size = cs.Constrain(dims.Size) - - offset := pos.Position(widgetSize, dims.Size) - dims.Baseline += offset.Y - return dims, f32.AffineId().Offset(layout.FPt(offset)) - } - case Fill: - } - - var scaledSize image.Point - scaledSize.X = int(float32(widgetSize.X) * scale.X) - scaledSize.Y = int(float32(widgetSize.Y) * scale.Y) - dims.Size = cs.Constrain(scaledSize) - dims.Baseline = int(float32(dims.Baseline) * scale.Y) - - offset := pos.Position(scaledSize, dims.Size) - trans := f32.AffineId(). - Scale(f32.Point{}, scale). - Offset(layout.FPt(offset)) - - dims.Baseline += offset.Y - - return dims, trans -} diff --git a/gio/widget/fit_test.go b/gio/widget/fit_test.go deleted file mode 100644 index f747bb2..0000000 --- a/gio/widget/fit_test.go +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/layout" -) - -func TestFit(t *testing.T) { - type test struct { - Dims image.Point - Scale f32.Point - Result image.Point - } - - fittests := [...][]test{ - Unscaled: { - { - Dims: image.Point{0, 0}, - Scale: f32.Point{X: 1, Y: 1}, - Result: image.Point{X: 0, Y: 0}, - }, { - Dims: image.Point{50, 25}, - Scale: f32.Point{X: 1, Y: 1}, - Result: image.Point{X: 50, Y: 25}, - }, { - Dims: image.Point{50, 200}, - Scale: f32.Point{X: 1, Y: 1}, - Result: image.Point{X: 50, Y: 100}, - }, - }, - Contain: { - { - Dims: image.Point{50, 25}, - Scale: f32.Point{X: 2, Y: 2}, - Result: image.Point{X: 100, Y: 50}, - }, { - Dims: image.Point{50, 200}, - Scale: f32.Point{X: 0.5, Y: 0.5}, - Result: image.Point{X: 25, Y: 100}, - }, - }, - Cover: { - { - Dims: image.Point{50, 25}, - Scale: f32.Point{X: 4, Y: 4}, - Result: image.Point{X: 100, Y: 100}, - }, { - Dims: image.Point{50, 200}, - Scale: f32.Point{X: 2, Y: 2}, - Result: image.Point{X: 100, Y: 100}, - }, - }, - ScaleDown: { - { - Dims: image.Point{50, 25}, - Scale: f32.Point{X: 1, Y: 1}, - Result: image.Point{X: 50, Y: 25}, - }, { - Dims: image.Point{50, 200}, - Scale: f32.Point{X: 0.5, Y: 0.5}, - Result: image.Point{X: 25, Y: 100}, - }, - }, - Fill: { - { - Dims: image.Point{50, 25}, - Scale: f32.Point{X: 2, Y: 4}, - Result: image.Point{X: 100, Y: 100}, - }, { - Dims: image.Point{50, 200}, - Scale: f32.Point{X: 2, Y: 0.5}, - Result: image.Point{X: 100, Y: 100}, - }, - }, - } - - for fit, tests := range fittests { - fit := Fit(fit) - for i, test := range tests { - cs := layout.Constraints{ - Max: image.Point{X: 100, Y: 100}, - } - result, trans := fit.scale(cs, layout.NW, layout.Dimensions{Size: test.Dims}) - sx, _, _, _, sy, _ := trans.Elems() - if scale := f32.Pt(sx, sy); scale != test.Scale { - t.Errorf("got scale %v expected %v", scale, test.Scale) - } - - if result.Size != test.Result { - t.Errorf("fit %v, #%v: expected %#v, got %#v", fit, i, test.Result, result.Size) - } - } - } -} diff --git a/gio/widget/float.go b/gio/widget/float.go deleted file mode 100644 index b79f8a4..0000000 --- a/gio/widget/float.go +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// Float is for selecting a value in a range. -type Float struct { - // Value is the value of the Float, in the [0; 1] range. - Value float32 - - drag gesture.Drag - axis layout.Axis - length float32 -} - -// Dragging returns whether the value is being interacted with. -func (f *Float) Dragging() bool { return f.drag.Dragging() } - -func (f *Float) Layout(gtx layout.Context, axis layout.Axis, pointerMargin unit.Dp) layout.Dimensions { - f.Update(gtx) - size := gtx.Constraints.Min - f.length = float32(axis.Convert(size).X) - f.axis = axis - - margin := axis.Convert(image.Pt(gtx.Dp(pointerMargin), 0)) - rect := image.Rectangle{ - Min: margin.Mul(-1), - Max: size.Add(margin), - } - defer clip.Rect(rect).Push(gtx.Ops).Pop() - f.drag.Add(gtx.Ops) - - return layout.Dimensions{Size: size} -} - -// Update the Value according to drag events along the f's main axis. -// The return value reports whether the value was changed. -// -// The range of f is set by the minimum constraints main axis value. -func (f *Float) Update(gtx layout.Context) bool { - changed := false - for { - e, ok := f.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(f.axis)) - if !ok { - break - } - if f.length > 0 && (e.Kind == pointer.Press || e.Kind == pointer.Drag) { - pos := e.Position.X - if f.axis == layout.Vertical { - pos = f.length - e.Position.Y - } - f.Value = pos / f.length - if f.Value < 0 { - f.Value = 0 - } else if f.Value > 1 { - f.Value = 1 - } - changed = true - } - } - return changed -} diff --git a/gio/widget/icon.go b/gio/widget/icon.go deleted file mode 100644 index 5da0b6c..0000000 --- a/gio/widget/icon.go +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - "image/color" - "image/draw" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" - - "golang.org/x/exp/shiny/iconvg" -) - -type Icon struct { - src []byte - // Cached values. - op paint.ImageOp - imgSize int - imgColor color.NRGBA -} - -const defaultIconSize = unit.Dp(24) - -// NewIcon returns a new Icon from IconVG data. -func NewIcon(data []byte) (*Icon, error) { - _, err := iconvg.DecodeMetadata(data) - if err != nil { - return nil, err - } - return &Icon{src: data}, nil -} - -// Layout displays the icon with its size set to the X minimum constraint. -func (ic *Icon) Layout(gtx layout.Context, color color.NRGBA) layout.Dimensions { - sz := gtx.Constraints.Min.X - if sz == 0 { - sz = gtx.Dp(defaultIconSize) - } - size := gtx.Constraints.Constrain(image.Pt(sz, sz)) - defer clip.Rect{Max: size}.Push(gtx.Ops).Pop() - - ico := ic.image(size.X, color) - ico.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - return layout.Dimensions{ - Size: ico.Size(), - } -} - -func (ic *Icon) image(sz int, color color.NRGBA) paint.ImageOp { - if sz == ic.imgSize && color == ic.imgColor { - return ic.op - } - m, _ := iconvg.DecodeMetadata(ic.src) - dx, dy := m.ViewBox.AspectRatio() - img := image.NewRGBA(image.Rectangle{Max: image.Point{X: sz, Y: int(float32(sz) * dy / dx)}}) - var ico iconvg.Rasterizer - ico.SetDstImage(img, img.Bounds(), draw.Src) - m.Palette[0] = f32color.NRGBAToLinearRGBA(color) - iconvg.Decode(&ico, ic.src, &iconvg.DecodeOptions{ - Palette: &m.Palette, - }) - ic.op = paint.NewImageOp(img) - ic.imgSize = sz - ic.imgColor = color - return ic.op -} diff --git a/gio/widget/icon_test.go b/gio/widget/icon_test.go deleted file mode 100644 index d185fcb..0000000 --- a/gio/widget/icon_test.go +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - "image/color" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - - "golang.org/x/exp/shiny/materialdesign/icons" -) - -func TestIcon_Alpha(t *testing.T) { - icon, err := NewIcon(icons.ToggleCheckBox) - if err != nil { - t.Fatal(err) - } - - col := color.NRGBA{B: 0xff, A: 0x40} - - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(100, 100)), - } - - _ = icon.Layout(gtx, col) -} - -// TestWidgetConstraints tests that widgets returns dimensions within their constraints. -func TestWidgetConstraints(t *testing.T) { - _cs := func(v ...layout.Constraints) []layout.Constraints { return v } - for _, tc := range []struct { - label string - widget layout.Widget - constraints []layout.Constraints - }{ - { - label: "Icon", - widget: func(gtx layout.Context) layout.Dimensions { - ic, _ := NewIcon(icons.ToggleCheckBox) - return ic.Layout(gtx, color.NRGBA{A: 0xff}) - }, - constraints: _cs( - layout.Constraints{ - Min: image.Pt(20, 0), - Max: image.Pt(100, 100), - }, - layout.Constraints{ - Max: image.Pt(100, 100), - }, - ), - }, - } { - t.Run(tc.label, func(t *testing.T) { - for _, cs := range tc.constraints { - gtx := layout.Context{ - Constraints: cs, - Ops: new(op.Ops), - } - dims := tc.widget(gtx) - csr := image.Rectangle{ - Min: cs.Min, - Max: cs.Max, - } - if !dims.Size.In(csr) { - t.Errorf("dims size %v not within constraints %v", dims.Size, csr) - } - } - }) - } -} diff --git a/gio/widget/image.go b/gio/widget/image.go deleted file mode 100644 index 1896af2..0000000 --- a/gio/widget/image.go +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// Image is a widget that displays an image. -type Image struct { - // Src is the image to display. - Src paint.ImageOp - // Fit specifies how to scale the image to the constraints. - // By default it does not do any scaling. - Fit Fit - // Position specifies where to position the image within - // the constraints. - Position layout.Direction - // Scale is the factor used for converting image pixels to dp. - // If Scale is zero it defaults to 1. - // - // To map one image pixel to one output pixel, set Scale to 1.0 / gtx.Metric.PxPerDp. - Scale float32 -} - -func (im Image) Layout(gtx layout.Context) layout.Dimensions { - scale := im.Scale - if scale == 0 { - scale = 1 - } - - size := im.Src.Size() - wf, hf := float32(size.X), float32(size.Y) - w, h := gtx.Dp(unit.Dp(wf*scale)), gtx.Dp(unit.Dp(hf*scale)) - - dims, trans := im.Fit.scale(gtx.Constraints, im.Position, layout.Dimensions{Size: image.Pt(w, h)}) - defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop() - - pixelScale := scale * gtx.Metric.PxPerDp - trans = trans.Mul(f32.AffineId().Scale(f32.Point{}, f32.Pt(pixelScale, pixelScale))) - defer op.Affine(trans).Push(gtx.Ops).Pop() - - im.Src.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - - return dims -} diff --git a/gio/widget/image_test.go b/gio/widget/image_test.go deleted file mode 100644 index a05568d..0000000 --- a/gio/widget/image_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -func TestImageScale(t *testing.T) { - var ops op.Ops - gtx := layout.Context{ - Ops: &ops, - Constraints: layout.Constraints{ - Max: image.Pt(50, 50), - }, - } - imgSize := image.Pt(10, 10) - img := image.NewNRGBA(image.Rectangle{Max: imgSize}) - imgOp := paint.NewImageOp(img) - - // Ensure the default scales correctly. - dims := Image{Src: imgOp}.Layout(gtx) - expectedSize := imgSize - expectedSize.X = int(float32(expectedSize.X)) - expectedSize.Y = int(float32(expectedSize.Y)) - if dims.Size != expectedSize { - t.Fatalf("non-scaled image is wrong size, expected %v, got %v", expectedSize, dims.Size) - } - - // Ensure scaling the image via the Scale field works. - currentScale := float32(0.5) - dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) - expectedSize = imgSize - expectedSize.X = int(float32(expectedSize.X) * currentScale) - expectedSize.Y = int(float32(expectedSize.Y) * currentScale) - if dims.Size != expectedSize { - t.Fatalf(".5 scale image is wrong size, expected %v, got %v", expectedSize, dims.Size) - } - - // Ensure the image responds to changes in DPI. - currentScale = float32(1) - gtx.Metric.PxPerDp = 2 - dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) - expectedSize = imgSize - expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) - expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) - if dims.Size != expectedSize { - t.Fatalf("HiDPI non-scaled image is wrong size, expected %v, got %v", expectedSize, dims.Size) - } - - // Ensure scaling the image responds to changes in DPI. - currentScale = float32(.5) - gtx.Metric.PxPerDp = 2 - dims = Image{Src: imgOp, Scale: float32(currentScale)}.Layout(gtx) - expectedSize = imgSize - expectedSize.X = int(float32(expectedSize.X) * currentScale * gtx.Metric.PxPerDp) - expectedSize.Y = int(float32(expectedSize.Y) * currentScale * gtx.Metric.PxPerDp) - if dims.Size != expectedSize { - t.Fatalf("HiDPI .5 scale image is wrong size, expected %v, got %v", expectedSize, dims.Size) - } -} diff --git a/gio/widget/index.go b/gio/widget/index.go deleted file mode 100644 index eef3133..0000000 --- a/gio/widget/index.go +++ /dev/null @@ -1,510 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "bufio" - "image" - "io" - "math" - "sort" - - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/go-text/typesetting/segmenter" - "golang.org/x/image/math/fixed" -) - -type lineInfo struct { - xOff fixed.Int26_6 - yOff int - width fixed.Int26_6 - ascent, descent fixed.Int26_6 - glyphs int -} - -type glyphIndex struct { - // glyphs holds the glyphs processed. - glyphs []text.Glyph - // positions contain all possible caret positions, sorted by rune index. - positions []combinedPos - // lines contains metadata about the size and position of each line of - // text. - lines []lineInfo - - // currentLineMin and currentLineMax track the dimensions of the line - // that is being indexed. - currentLineMin, currentLineMax fixed.Int26_6 - // currentLineGlyphs tracks how many glyphs are contained within the - // line that is being indexed. - currentLineGlyphs int - // pos tracks attributes of the next valid cursor position within the indexed - // text. - pos combinedPos - // prog tracks the current glyph text progression to detect bidi changes. - prog text.Flags - // clusterAdvance accumulates the advances of glyphs in a glyph cluster. - clusterAdvance fixed.Int26_6 - // truncated indicates that the text was truncated by the shaper. - truncated bool - // midCluster tracks whether the next glyph processed is not the first glyph in a - // cluster. - midCluster bool -} - -// reset prepares the index for reuse. -func (g *glyphIndex) reset() { - g.glyphs = g.glyphs[:0] - g.positions = g.positions[:0] - g.lines = g.lines[:0] - g.currentLineMin = 0 - g.currentLineMax = 0 - g.currentLineGlyphs = 0 - g.pos = combinedPos{} - g.prog = 0 - g.clusterAdvance = 0 - g.truncated = false - g.midCluster = false -} - -// screenPos represents a character position in text line and column numbers, -// not pixels. -type screenPos struct { - // col is the column, measured in runes. - // FIXME: we only ever use col for start or end of lines. - // We don't need accurate accounting, so can we get rid of it? - col int - line int -} - -// combinedPos is a point in the editor. -type combinedPos struct { - // runes is the offset in runes. - runes int - - lineCol screenPos - - // Pixel coordinates - x fixed.Int26_6 - y int - - ascent, descent fixed.Int26_6 - - // runIndex tracks which run this position is within, counted each time - // the index processes an end of run marker. - runIndex int - // towardOrigin tracks whether this glyph's run is progressing toward the - // origin or away from it. - towardOrigin bool -} - -// incrementPosition returns the next position after pos (if any). Pos _must_ be -// an unmodified position acquired from one of the closest* methods. If eof is -// true, there was no next position. -func (g *glyphIndex) incrementPosition(pos combinedPos) (next combinedPos, eof bool) { - candidate, index := g.closestToRune(pos.runes) - for candidate != pos && index+1 < len(g.positions) { - index++ - candidate = g.positions[index] - } - if index+1 < len(g.positions) { - return g.positions[index+1], false - } - return candidate, true -} - -func (g *glyphIndex) insertPosition(pos combinedPos) { - lastIdx := len(g.positions) - 1 - if lastIdx >= 0 { - lastPos := g.positions[lastIdx] - if lastPos.runes == pos.runes && (lastPos.y != pos.y || (lastPos.x == pos.x)) { - // If we insert a consecutive position with the same logical position, - // overwrite the previous position with the new one. - g.positions[lastIdx] = pos - return - } - } - g.positions = append(g.positions, pos) -} - -// Glyph indexes the provided glyph, generating text cursor positions for it. -func (g *glyphIndex) Glyph(gl text.Glyph) { - g.glyphs = append(g.glyphs, gl) - g.currentLineGlyphs++ - if len(g.positions) == 0 { - // First-iteration setup. - g.currentLineMin = math.MaxInt32 - g.currentLineMax = 0 - } - if gl.X < g.currentLineMin { - g.currentLineMin = gl.X - } - if end := gl.X + gl.Advance; end > g.currentLineMax { - g.currentLineMax = end - } - - needsNewLine := gl.Flags&text.FlagLineBreak != 0 - needsNewRun := gl.Flags&text.FlagRunBreak != 0 - breaksParagraph := gl.Flags&text.FlagParagraphBreak != 0 - breaksCluster := gl.Flags&text.FlagClusterBreak != 0 - // We should insert new positions if the glyph we're processing terminates - // a glyph cluster, has nonzero runes, and is not a hard newline. - insertPositionsWithin := breaksCluster && !breaksParagraph && gl.Runes > 0 - - // Get the text progression/direction right. - g.prog = gl.Flags & text.FlagTowardOrigin - g.pos.towardOrigin = g.prog == text.FlagTowardOrigin - if !g.midCluster { - // Create the text position prior to the glyph. - g.pos.x = gl.X - g.pos.y = int(gl.Y) - g.pos.ascent = gl.Ascent - g.pos.descent = gl.Descent - if g.pos.towardOrigin { - g.pos.x += gl.Advance - } - g.insertPosition(g.pos) - } - - g.midCluster = !breaksCluster - - if breaksParagraph { - // Paragraph breaking clusters shouldn't have positions generated for both - // sides of them. They're always zero-width, so doing so would - // create two visually identical cursor positions. Just reset - // cluster state, increment by their runes, and move on to the - // next glyph. - g.clusterAdvance = 0 - g.pos.runes += int(gl.Runes) - } - // Always track the cumulative advance added by the glyph, even if it - // doesn't terminate a cluster itself. - g.clusterAdvance += gl.Advance - if insertPositionsWithin { - // Construct the text positions _within_ gl. - g.pos.y = int(gl.Y) - g.pos.ascent = gl.Ascent - g.pos.descent = gl.Descent - width := g.clusterAdvance - positionCount := int(gl.Runes) - runesPerPosition := 1 - if gl.Flags&text.FlagTruncator != 0 { - // Treat the truncator as a single unit that is either selected or not. - positionCount = 1 - runesPerPosition = int(gl.Runes) - g.truncated = true - } - perRune := width / fixed.Int26_6(positionCount) - adjust := fixed.Int26_6(0) - if g.pos.towardOrigin { - // If RTL, subtract increments from the width of the cluster - // instead of adding. - adjust = width - perRune = -perRune - } - for i := 1; i <= positionCount; i++ { - g.pos.x = gl.X + adjust + perRune*fixed.Int26_6(i) - g.pos.runes += runesPerPosition - g.pos.lineCol.col += runesPerPosition - g.insertPosition(g.pos) - } - g.clusterAdvance = 0 - } - if needsNewRun { - g.pos.runIndex++ - } - if needsNewLine { - g.lines = append(g.lines, lineInfo{ - xOff: g.currentLineMin, - yOff: int(gl.Y), - width: g.currentLineMax - g.currentLineMin, - ascent: g.positions[len(g.positions)-1].ascent, - descent: g.positions[len(g.positions)-1].descent, - glyphs: g.currentLineGlyphs, - }) - g.pos.lineCol.line++ - g.pos.lineCol.col = 0 - g.pos.runIndex = 0 - g.currentLineMin = math.MaxInt32 - g.currentLineMax = 0 - g.currentLineGlyphs = 0 - } -} - -func (g *glyphIndex) closestToRune(runeIdx int) (combinedPos, int) { - if len(g.positions) == 0 { - return combinedPos{}, 0 - } - i := sort.Search(len(g.positions), func(i int) bool { - pos := g.positions[i] - return pos.runes >= runeIdx - }) - if i > 0 { - i-- - } - closest := g.positions[i] - closestI := i - for ; i < len(g.positions); i++ { - if g.positions[i].runes == runeIdx { - return g.positions[i], i - } - } - return closest, closestI -} - -func (g *glyphIndex) closestToLineCol(lineCol screenPos) combinedPos { - if len(g.positions) == 0 { - return combinedPos{} - } - i := sort.Search(len(g.positions), func(i int) bool { - pos := g.positions[i] - return pos.lineCol.line > lineCol.line || (pos.lineCol.line == lineCol.line && pos.lineCol.col >= lineCol.col) - }) - if i > 0 { - i-- - } - prior := g.positions[i] - if i+1 >= len(g.positions) { - return prior - } - next := g.positions[i+1] - if next.lineCol != lineCol { - return prior - } - return next -} - -func dist(a, b fixed.Int26_6) fixed.Int26_6 { - if a > b { - return a - b - } - return b - a -} - -func (g *glyphIndex) closestToXY(x fixed.Int26_6, y int) combinedPos { - if len(g.positions) == 0 { - return combinedPos{} - } - i := sort.Search(len(g.positions), func(i int) bool { - pos := g.positions[i] - return pos.y+pos.descent.Round() >= y - }) - // If no position was greater than the provided Y, the text is too - // short. Return either the last position or (if there are no - // positions) the zero position. - if i == len(g.positions) { - return g.positions[i-1] - } - first := g.positions[i] - // Find the best X coordinate. - closest := i - closestDist := dist(first.x, x) - line := first.lineCol.line - // NOTE(whereswaldon): there isn't a simple way to accelerate this. Bidi text means that the x coordinates - // for positions have no fixed relationship. In the future, we can consider sorting the positions - // on a line by their x coordinate and caching that. It'll be a one-time O(nlogn) per line, but - // subsequent uses of this function for that line become O(logn). Right now it's always O(n). - for i := i + 1; i < len(g.positions) && g.positions[i].lineCol.line == line; i++ { - candidate := g.positions[i] - distance := dist(candidate.x, x) - // If we are *really* close to the current position candidate, just choose it. - if distance.Round() == 0 { - return g.positions[i] - } - if distance < closestDist { - closestDist = distance - closest = i - } - } - return g.positions[closest] -} - -// makeRegion creates a text-aligned rectangle from start to end. The vertical -// dimensions of the rectangle are derived from the provided line's ascent and -// descent, and the y offset of the line's baseline is provided as y. -func makeRegion(line lineInfo, y int, start, end fixed.Int26_6) Region { - if start > end { - start, end = end, start - } - dotStart := image.Pt(start.Round(), y) - dotEnd := image.Pt(end.Round(), y) - return Region{ - Bounds: image.Rectangle{ - Min: dotStart.Sub(image.Point{Y: line.ascent.Ceil()}), - Max: dotEnd.Add(image.Point{Y: line.descent.Floor()}), - }, - Baseline: line.descent.Floor(), - } -} - -// Region describes the position and baseline of an area of interest within -// shaped text. -type Region struct { - // Bounds is the coordinates of the bounding box relative to the containing - // widget. - Bounds image.Rectangle - // Baseline is the quantity of vertical pixels between the baseline and - // the bottom of bounds. - Baseline int -} - -// locate returns highlight regions covering the glyphs that represent the runes in -// [startRune,endRune). If the rects parameter is non-nil, locate will use it to -// return results instead of allocating, provided that there is enough capacity. -// The returned regions have their Bounds specified relative to the provided -// viewport. -func (g *glyphIndex) locate(viewport image.Rectangle, startRune, endRune int, rects []Region) []Region { - if startRune > endRune { - startRune, endRune = endRune, startRune - } - rects = rects[:0] - caretStart, _ := g.closestToRune(startRune) - caretEnd, _ := g.closestToRune(endRune) - - for lineIdx := caretStart.lineCol.line; lineIdx < len(g.lines); lineIdx++ { - if lineIdx > caretEnd.lineCol.line { - break - } - pos := g.closestToLineCol(screenPos{line: lineIdx}) - if int(pos.y)+pos.descent.Ceil() < viewport.Min.Y { - continue - } - if int(pos.y)-pos.ascent.Ceil() > viewport.Max.Y { - break - } - line := g.lines[lineIdx] - if lineIdx > caretStart.lineCol.line && lineIdx < caretEnd.lineCol.line { - startX := line.xOff - endX := startX + line.width - // The entire line is selected. - rects = append(rects, makeRegion(line, pos.y, startX, endX)) - continue - } - selectionStart := caretStart - selectionEnd := caretEnd - if lineIdx != caretStart.lineCol.line { - // This line does not contain the beginning of the selection. - selectionStart = g.closestToLineCol(screenPos{line: lineIdx}) - } - if lineIdx != caretEnd.lineCol.line { - // This line does not contain the end of the selection. - selectionEnd = g.closestToLineCol(screenPos{line: lineIdx, col: math.MaxInt}) - } - - var ( - startX, endX fixed.Int26_6 - eof bool - ) - lineLoop: - for !eof { - startX = selectionStart.x - if selectionStart.runIndex == selectionEnd.runIndex { - // Commit selection. - endX = selectionEnd.x - rects = append(rects, makeRegion(line, pos.y, startX, endX)) - break - } else { - currentDirection := selectionStart.towardOrigin - previous := selectionStart - runLoop: - for !eof { - // Increment the start position until the next logical run. - for startRun := selectionStart.runIndex; selectionStart.runIndex == startRun; { - previous = selectionStart - selectionStart, eof = g.incrementPosition(selectionStart) - if eof { - endX = selectionStart.x - rects = append(rects, makeRegion(line, pos.y, startX, endX)) - break runLoop - } - } - if selectionStart.towardOrigin != currentDirection { - endX = previous.x - rects = append(rects, makeRegion(line, pos.y, startX, endX)) - break - } - if selectionStart.runIndex == selectionEnd.runIndex { - // Commit selection. - endX = selectionEnd.x - rects = append(rects, makeRegion(line, pos.y, startX, endX)) - break lineLoop - } - } - } - } - } - for i := range rects { - rects[i].Bounds = rects[i].Bounds.Sub(viewport.Min) - } - return rects -} - -// graphemeReader segments paragraphs of text into grapheme clusters. -type graphemeReader struct { - segmenter.Segmenter - graphemes []int - paragraph []rune - source io.ReaderAt - cursor int64 - reader *bufio.Reader - runeOffset int -} - -// SetSource configures the reader to pull from source. -func (p *graphemeReader) SetSource(source io.ReaderAt) { - p.source = source - p.cursor = 0 - p.reader = bufio.NewReader(p) - p.runeOffset = 0 -} - -// Read exists to satisfy io.Reader. It should not be directly invoked. -func (p *graphemeReader) Read(b []byte) (int, error) { - n, err := p.source.ReadAt(b, p.cursor) - p.cursor += int64(n) - return n, err -} - -// next decodes one paragraph of rune data. -func (p *graphemeReader) next() ([]rune, bool) { - p.paragraph = p.paragraph[:0] - var err error - var r rune - for err == nil { - r, _, err = p.reader.ReadRune() - if err != nil { - break - } - p.paragraph = append(p.paragraph, r) - if r == '\n' { - break - } - } - return p.paragraph, err == nil -} - -// Graphemes will return the next paragraph's grapheme cluster boundaries, -// if any. If it returns an empty slice, there is no more data (all paragraphs -// have been segmented). -func (p *graphemeReader) Graphemes() []int { - var more bool - p.graphemes = p.graphemes[:0] - p.paragraph, more = p.next() - if len(p.paragraph) == 0 && !more { - return nil - } - p.Segmenter.Init(p.paragraph) - iter := p.Segmenter.GraphemeIterator() - if iter.Next() { - graph := iter.Grapheme() - p.graphemes = append(p.graphemes, - p.runeOffset+graph.Offset, - p.runeOffset+graph.Offset+len(graph.Text), - ) - } - for iter.Next() { - graph := iter.Grapheme() - p.graphemes = append(p.graphemes, p.runeOffset+graph.Offset+len(graph.Text)) - } - p.runeOffset += len(p.paragraph) - return p.graphemes -} diff --git a/gio/widget/index_test.go b/gio/widget/index_test.go deleted file mode 100644 index 473f0aa..0000000 --- a/gio/widget/index_test.go +++ /dev/null @@ -1,895 +0,0 @@ -package widget - -import ( - "bytes" - "image" - "image/png" - "io" - "os" - "testing" - - nsareg "eliasnaur.com/font/noto/sans/arabic/regular" - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/font/opentype" - "github.com/p9c/p9/pkg/gel/gio/gpu/headless" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/text" - "golang.org/x/image/font/gofont/goregular" - "golang.org/x/image/math/fixed" -) - -// makePosTestText returns two bidi samples of shaped text at the given -// font size and wrapped to the given line width. The runeLimit, if nonzero, -// truncates the sample text to ensure shorter output for expensive tests. -func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (shaper *text.Shaper, source string, bidiLTR, bidiRTL []text.Glyph) { - ltrFace, _ := opentype.Parse(goregular.TTF) - rtlFace, _ := opentype.Parse(nsareg.TTF) - - shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{ - { - Font: font.Font{Typeface: "LTR"}, - Face: ltrFace, - }, - { - Font: font.Font{Typeface: "RTL"}, - Face: rtlFace, - }, - })) - // bidiSource is crafted to contain multiple consecutive RTL runs (by - // changing scripts within the RTL). - bidiSource := "The quick سماء שלום لا fox تمط שלום غير the lazy dog." - ltrParams := text.Parameters{ - PxPerEm: fixed.I(fontSize), - MaxWidth: lineWidth, - MinWidth: lineWidth, - Locale: english, - } - rtlParams := text.Parameters{ - Alignment: text.End, - PxPerEm: fixed.I(fontSize), - MaxWidth: lineWidth, - MinWidth: lineWidth, - Locale: arabic, - } - if alignOpposite { - ltrParams.Alignment = text.End - rtlParams.Alignment = text.Start - } - shaper.LayoutString(ltrParams, bidiSource) - for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { - bidiLTR = append(bidiLTR, g) - } - shaper.LayoutString(rtlParams, bidiSource) - for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { - bidiRTL = append(bidiRTL, g) - } - return shaper, bidiSource, bidiLTR, bidiRTL -} - -// makeAccountingTestText shapes text designed to stress rune accounting -// logic within the index. -func makeAccountingTestText(str string, fontSize, lineWidth int) (txt []text.Glyph) { - ltrFace, _ := opentype.Parse(goregular.TTF) - rtlFace, _ := opentype.Parse(nsareg.TTF) - - shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{ - { - Font: font.Font{Typeface: "LTR"}, - Face: ltrFace, - }, - { - Font: font.Font{Typeface: "RTL"}, - Face: rtlFace, - }, - })) - params := text.Parameters{ - PxPerEm: fixed.I(fontSize), - MaxWidth: lineWidth, - Locale: english, - } - shaper.LayoutString(params, str) - for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { - txt = append(txt, g) - } - return txt -} - -// getGlyphs shapes text as english. -func getGlyphs(fontSize, minWidth, lineWidth int, align text.Alignment, str string) (txt []text.Glyph) { - ltrFace, _ := opentype.Parse(goregular.TTF) - rtlFace, _ := opentype.Parse(nsareg.TTF) - - shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{ - { - Font: font.Font{Typeface: "LTR"}, - Face: ltrFace, - }, - { - Font: font.Font{Typeface: "RTL"}, - Face: rtlFace, - }, - })) - params := text.Parameters{ - PxPerEm: fixed.I(fontSize), - Alignment: align, - MinWidth: minWidth, - MaxWidth: lineWidth, - Locale: english, - WrapPolicy: text.WrapWords, - } - shaper.LayoutString(params, str) - for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() { - txt = append(txt, g) - } - return txt -} - -// TestIndexPositionWhitespace checks that the index correctly generates cursor positions -// for empty lines and the empty string. -func TestIndexPositionWhitespace(t *testing.T) { - type testcase struct { - name string - str string - lineWidth int - align text.Alignment - expected []combinedPos - } - for _, tc := range []testcase{ - { - name: "empty string", - str: "", - lineWidth: 200, - expected: []combinedPos{ - {x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)}, - }, - }, - { - name: "just hard newline", - str: "\n", - lineWidth: 200, - expected: []combinedPos{ - {x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)}, - {x: fixed.Int26_6(0), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}}, - }, - }, - { - name: "trailing newline", - str: "a\n", - lineWidth: 200, - expected: []combinedPos{ - {x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)}, - {x: fixed.Int26_6(570), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{col: 1}}, - {x: fixed.Int26_6(0), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 2, lineCol: screenPos{line: 1}}, - }, - }, - { - name: "just blank line", - str: "\n\n", - lineWidth: 200, - expected: []combinedPos{ - {x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)}, - {x: fixed.Int26_6(0), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}}, - {x: fixed.Int26_6(0), y: 54, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 2, lineCol: screenPos{line: 2}}, - }, - }, - { - name: "middle aligned blank lines", - str: "\n\n\nabc", - align: text.Middle, - lineWidth: 200, - expected: []combinedPos{ - {x: fixed.Int26_6(832), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)}, - {x: fixed.Int26_6(832), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{line: 1}}, - {x: fixed.Int26_6(832), y: 54, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 2, lineCol: screenPos{line: 2}}, - {x: fixed.Int26_6(6), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 3, lineCol: screenPos{line: 3}}, - {x: fixed.Int26_6(576), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 4, lineCol: screenPos{line: 3, col: 1}}, - {x: fixed.Int26_6(1146), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 5, lineCol: screenPos{line: 3, col: 2}}, - {x: fixed.Int26_6(1658), y: 73, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 6, lineCol: screenPos{line: 3, col: 3}}, - }, - }, - { - name: "blank line", - str: "a\n\nb", - lineWidth: 200, - expected: []combinedPos{ - {x: fixed.Int26_6(0), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216)}, - {x: fixed.Int26_6(570), y: 16, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 1, lineCol: screenPos{col: 1}}, - {x: fixed.Int26_6(0), y: 35, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 2, lineCol: screenPos{line: 1}}, - {x: fixed.Int26_6(0), y: 54, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 3, lineCol: screenPos{line: 2}}, - {x: fixed.Int26_6(570), y: 54, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), runes: 4, lineCol: screenPos{line: 2, col: 1}}, - }, - }, - { - name: "soft wrap", - str: "abc def", - lineWidth: 30, - expected: []combinedPos{ - {runes: 0, lineCol: screenPos{line: 0, col: 0}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 0, y: 16}, - {runes: 1, lineCol: screenPos{line: 0, col: 1}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 570, y: 16}, - {runes: 2, lineCol: screenPos{line: 0, col: 2}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 1140, y: 16}, - {runes: 3, lineCol: screenPos{line: 0, col: 3}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 1652, y: 16}, - {runes: 4, lineCol: screenPos{line: 1, col: 0}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 0, y: 35}, - {runes: 5, lineCol: screenPos{line: 1, col: 1}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 570, y: 35}, - {runes: 6, lineCol: screenPos{line: 1, col: 2}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 1140, y: 35}, - {runes: 7, lineCol: screenPos{line: 1, col: 3}, ascent: fixed.Int26_6(968), descent: fixed.Int26_6(216), x: 1425, y: 35}, - }, - }, - { - name: "soft wrap arabic", - str: "ثنائي الاتجاه", - lineWidth: 30, - expected: []combinedPos{ - {runes: 0, lineCol: screenPos{line: 0, col: 0}, ascent: 1407, descent: 756, x: 2250, y: 22, towardOrigin: true}, - {runes: 1, lineCol: screenPos{line: 0, col: 1}, ascent: 1407, descent: 756, x: 1944, y: 22, towardOrigin: true}, - {runes: 2, lineCol: screenPos{line: 0, col: 2}, ascent: 1407, descent: 756, x: 1593, y: 22, towardOrigin: true}, - {runes: 3, lineCol: screenPos{line: 0, col: 3}, ascent: 1407, descent: 756, x: 1295, y: 22, towardOrigin: true}, - {runes: 4, lineCol: screenPos{line: 0, col: 4}, ascent: 1407, descent: 756, x: 1020, y: 22, towardOrigin: true}, - {runes: 5, lineCol: screenPos{line: 0, col: 5}, ascent: 1407, descent: 756, x: 266, y: 22, towardOrigin: true}, - {runes: 6, lineCol: screenPos{line: 1, col: 0}, ascent: 1407, descent: 756, x: 2511, y: 41, towardOrigin: true}, - {runes: 7, lineCol: screenPos{line: 1, col: 1}, ascent: 1407, descent: 756, x: 2267, y: 41, towardOrigin: true}, - {runes: 8, lineCol: screenPos{line: 1, col: 2}, ascent: 1407, descent: 756, x: 1969, y: 41, towardOrigin: true}, - {runes: 9, lineCol: screenPos{line: 1, col: 3}, ascent: 1407, descent: 756, x: 1671, y: 41, towardOrigin: true}, - {runes: 10, lineCol: screenPos{line: 1, col: 4}, ascent: 1407, descent: 756, x: 1365, y: 41, towardOrigin: true}, - {runes: 11, lineCol: screenPos{line: 1, col: 5}, ascent: 1407, descent: 756, x: 713, y: 41, towardOrigin: true}, - {runes: 12, lineCol: screenPos{line: 1, col: 6}, ascent: 1407, descent: 756, x: 415, y: 41, towardOrigin: true}, - {runes: 13, lineCol: screenPos{line: 1, col: 7}, ascent: 1407, descent: 756, x: 0, y: 41, towardOrigin: true}, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - glyphs := getGlyphs(16, 0, tc.lineWidth, tc.align, tc.str) - var gi glyphIndex - gi.reset() - for _, g := range glyphs { - gi.Glyph(g) - } - if len(gi.positions) != len(tc.expected) { - t.Errorf("expected %d positions, got %d", len(tc.expected), len(gi.positions)) - } - for i := range min(len(gi.positions), len(tc.expected)) { - actual := gi.positions[i] - expected := tc.expected[i] - if actual != expected { - t.Errorf("position %d: expected:\n%#+v, got:\n%#+v", i, expected, actual) - } - } - if t.Failed() { - printPositions(t, gi.positions) - printGlyphs(t, glyphs) - } - }) - } -} - -// TestIndexPositionBidi tests whether the index correct generates cursor positions for -// complex bidirectional text. -func TestIndexPositionBidi(t *testing.T) { - fontSize := 16 - lineWidth := fontSize * 10 - shaper, _, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false) - type testcase struct { - name string - glyphs []text.Glyph - expectedXs []fixed.Int26_6 - } - for _, tc := range []testcase{ - { - name: "bidi ltr", - glyphs: bidiLTRText, - expectedXs: []fixed.Int26_6{ - 0, 626, 1196, 1766, 2051, 2621, 3191, 3444, 3956, 4468, 4753, 7133, 6330, 5738, 5440, 5019, // Positions on line 0. - - 3953, 3185, 2417, 1649, 881, 596, 298, 0, 3953, 4238, 4523, 5093, 5605, 5890, 7905, 7599, 7007, 6156, // Positions on line 1. - - 4660, 3892, 3124, 2356, 1588, 1303, 788, 406, 0, 4660, 4945, 5235, 5805, 6375, 6660, 6934, 7504, 8016, 8528, // Positions on line 2. - - 0, 570, 1140, 1710, 2034, // Positions on line 3. - }, - }, - { - name: "bidi rtl", - glyphs: bidiRTLText, - expectedXs: []fixed.Int26_6{ - // Line 0 - 5718, 6344, 6914, 7484, 7769, 8339, 8909, 9162, 9674, 10186, 5718, 5452, 4649, 4057, 3759, 3338, 3072, 2304, 1536, 768, 0, - // Line 1 - 9170, 8872, 8574, 8308, 6941, 7226, 7796, 8308, 6941, 6675, 6369, 5777, 4926, 4660, 3892, 3124, 2356, 1588, 1303, 788, 406, 0, - // Line 2 - 324, 614, 1184, 1754, 2039, 2313, 2883, 3395, 3907, 4192, 4762, 5332, 5902, 324, 0, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - var gi glyphIndex - gi.reset() - for _, g := range tc.glyphs { - gi.Glyph(g) - } - if len(gi.positions) != len(tc.expectedXs) { - t.Errorf("expected %d positions, got %d", len(tc.expectedXs), len(gi.positions)) - } - lastRunes := 0 - lastLine := 0 - lastCol := -1 - lastY := 0 - for i := range min(len(gi.positions), len(tc.expectedXs)) { - actualX := gi.positions[i].x - expectedX := tc.expectedXs[i] - if actualX != expectedX { - t.Errorf("position %d: expected x=%v(%d), got x=%v(%d)", i, expectedX, expectedX, actualX, actualX) - } - if r := gi.positions[i].runes; r < lastRunes { - t.Errorf("position %d: expected runes >= %d, got %d", i, lastRunes, r) - } - lastRunes = gi.positions[i].runes - if y := gi.positions[i].y; y < lastY { - t.Errorf("position %d: expected y>= %d, got %d", i, lastY, y) - } - lastY = gi.positions[i].y - if y := gi.positions[i].y; y < lastY { - t.Errorf("position %d: expected y>= %d, got %d", i, lastY, y) - } - lastY = gi.positions[i].y - if lineCol := gi.positions[i].lineCol; lineCol.line == lastLine && lineCol.col < lastCol { - t.Errorf("position %d: expected col >= %d, got %d", i, lastCol, lineCol.col) - } - lastCol = gi.positions[i].lineCol.col - if line := gi.positions[i].lineCol.line; line < lastLine { - t.Errorf("position %d: expected line >= %d, got %d", i, lastLine, line) - } - lastLine = gi.positions[i].lineCol.line - } - printPositions(t, gi.positions) - if t.Failed() { - printGlyphs(t, tc.glyphs) - width := lineWidth - height := 100 - cap := image.NewRGBA(image.Rect(0, 0, width, height)) - w, _ := headless.NewWindow(width, height) - defer w.Release() - ops := new(op.Ops) - gtx := layout.Context{ - Constraints: layout.Constraints{Max: image.Pt(width, height)}, - Ops: ops, - } - it := textIterator{viewport: image.Rectangle{Max: image.Point{X: width, Y: height}}} - for _, g := range tc.glyphs { - it.processGlyph(g, true) - } - var glyphs [32]text.Glyph - line := glyphs[:0] - for _, g := range gi.glyphs { - var ok bool - if line, ok = it.paintGlyph(gtx, shaper, g, line); !ok { - break - } - } - w.Frame(ops) - w.Screenshot(cap) - b := new(bytes.Buffer) - _ = png.Encode(b, cap) - screenshotName := tc.name + ".png" - _ = os.WriteFile(screenshotName, b.Bytes(), 0o644) - t.Logf("wrote %q", screenshotName) - } - }) - } -} - -func TestIndexPositionLines(t *testing.T) { - fontSize := 16 - lineWidth := fontSize * 10 - _, source1, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false) - _, source2, bidiLTRTextOpp, bidiRTLTextOpp := makePosTestText(fontSize, lineWidth, true) - type testcase struct { - name string - source string - glyphs []text.Glyph - expectedLines []lineInfo - } - for _, tc := range []testcase{ - { - name: "bidi ltr", - source: source1, - glyphs: bidiLTRText, - expectedLines: []lineInfo{ - { - xOff: fixed.Int26_6(0), - yOff: 22, - glyphs: 15, - width: fixed.Int26_6(7133), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(0), - yOff: 41, - glyphs: 15, - width: fixed.Int26_6(7905), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(0), - yOff: 60, - glyphs: 18, - width: fixed.Int26_6(8528), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(0), - yOff: 79, - glyphs: 4, - width: fixed.Int26_6(2034), - ascent: fixed.Int26_6(968), - descent: fixed.Int26_6(216), - }, - }, - }, - { - name: "bidi rtl", - source: source1, - glyphs: bidiRTLText, - expectedLines: []lineInfo{ - { - xOff: fixed.Int26_6(0), - yOff: 22, - glyphs: 20, - width: fixed.Int26_6(10186), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(0), - yOff: 41, - glyphs: 19, - width: fixed.Int26_6(9170), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(0), - yOff: 60, - glyphs: 13, - width: fixed.Int26_6(5902), - ascent: fixed.Int26_6(968), - descent: fixed.Int26_6(216), - }, - }, - }, - { - name: "bidi ltr opposite alignment", - source: source2, - glyphs: bidiLTRTextOpp, - expectedLines: []lineInfo{ - { - xOff: fixed.Int26_6(3107), - yOff: 22, - glyphs: 15, - width: fixed.Int26_6(7133), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(2335), - yOff: 41, - glyphs: 15, - width: fixed.Int26_6(7905), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(1712), - yOff: 60, - glyphs: 18, - width: fixed.Int26_6(8528), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(8206), - yOff: 79, - glyphs: 4, - width: fixed.Int26_6(2034), - ascent: fixed.Int26_6(968), - descent: fixed.Int26_6(216), - }, - }, - }, - { - name: "bidi rtl opposite alignment", - source: source2, - glyphs: bidiRTLTextOpp, - expectedLines: []lineInfo{ - { - xOff: fixed.Int26_6(54), - yOff: 22, - glyphs: 20, - width: fixed.Int26_6(10186), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(1070), - yOff: 41, - glyphs: 19, - width: fixed.Int26_6(9170), - ascent: fixed.Int26_6(1407), - descent: fixed.Int26_6(756), - }, - { - xOff: fixed.Int26_6(4338), - yOff: 60, - glyphs: 13, - width: fixed.Int26_6(5902), - ascent: fixed.Int26_6(968), - descent: fixed.Int26_6(216), - }, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - var gi glyphIndex - gi.reset() - for _, g := range tc.glyphs { - gi.Glyph(g) - } - if len(gi.lines) != len(tc.expectedLines) { - t.Errorf("expected %d lines, got %d", len(tc.expectedLines), len(gi.lines)) - } - for i := range min(len(gi.lines), len(tc.expectedLines)) { - actual := gi.lines[i] - expected := tc.expectedLines[i] - if actual != expected { - t.Errorf("line %d: expected:\n%#+v, got:\n%#+v", i, expected, actual) - } - } - }) - } -} - -// TestIndexPositionRunes checks for rune accounting errors in positions -// generated by the index. -func TestIndexPositionRunes(t *testing.T) { - fontSize := 16 - lineWidth := fontSize * 10 - // source is crafted to contain multiple consecutive RTL runs (by - // changing scripts within the RTL). - source := "The\nquick سماء של\nום لا fox\nتمط של\nום." - testText := makeAccountingTestText(source, fontSize, lineWidth) - type testcase struct { - name string - source string - glyphs []text.Glyph - expected []combinedPos - } - for _, tc := range []testcase{ - { - name: "many newlines", - source: source, - glyphs: testText, - expected: []combinedPos{ - {runes: 0, lineCol: screenPos{line: 0, col: 0}, runIndex: 0, towardOrigin: false}, - {runes: 1, lineCol: screenPos{line: 0, col: 1}, runIndex: 0, towardOrigin: false}, - {runes: 2, lineCol: screenPos{line: 0, col: 2}, runIndex: 0, towardOrigin: false}, - {runes: 3, lineCol: screenPos{line: 0, col: 3}, runIndex: 0, towardOrigin: false}, - {runes: 4, lineCol: screenPos{line: 1, col: 0}, runIndex: 0, towardOrigin: false}, - {runes: 5, lineCol: screenPos{line: 1, col: 1}, runIndex: 0, towardOrigin: false}, - {runes: 6, lineCol: screenPos{line: 1, col: 2}, runIndex: 0, towardOrigin: false}, - {runes: 7, lineCol: screenPos{line: 1, col: 3}, runIndex: 0, towardOrigin: false}, - {runes: 8, lineCol: screenPos{line: 1, col: 4}, runIndex: 0, towardOrigin: false}, - {runes: 9, lineCol: screenPos{line: 1, col: 5}, runIndex: 0, towardOrigin: false}, - {runes: 10, lineCol: screenPos{line: 1, col: 6}, runIndex: 0, towardOrigin: false}, - {runes: 10, lineCol: screenPos{line: 1, col: 6}, runIndex: 1, towardOrigin: true}, - {runes: 11, lineCol: screenPos{line: 1, col: 7}, runIndex: 1, towardOrigin: true}, - {runes: 12, lineCol: screenPos{line: 1, col: 8}, runIndex: 1, towardOrigin: true}, - {runes: 13, lineCol: screenPos{line: 1, col: 9}, runIndex: 1, towardOrigin: true}, - {runes: 14, lineCol: screenPos{line: 1, col: 10}, runIndex: 1, towardOrigin: true}, - {runes: 15, lineCol: screenPos{line: 1, col: 11}, runIndex: 2, towardOrigin: true}, - {runes: 16, lineCol: screenPos{line: 1, col: 12}, runIndex: 2, towardOrigin: true}, - {runes: 17, lineCol: screenPos{line: 1, col: 13}, runIndex: 2, towardOrigin: true}, - {runes: 18, lineCol: screenPos{line: 2, col: 0}, runIndex: 0, towardOrigin: true}, - {runes: 19, lineCol: screenPos{line: 2, col: 1}, runIndex: 0, towardOrigin: true}, - {runes: 20, lineCol: screenPos{line: 2, col: 2}, runIndex: 0, towardOrigin: true}, - {runes: 21, lineCol: screenPos{line: 2, col: 3}, runIndex: 1, towardOrigin: true}, - {runes: 22, lineCol: screenPos{line: 2, col: 4}, runIndex: 1, towardOrigin: true}, - {runes: 23, lineCol: screenPos{line: 2, col: 5}, runIndex: 1, towardOrigin: true}, - {runes: 24, lineCol: screenPos{line: 2, col: 6}, runIndex: 1, towardOrigin: true}, - {runes: 24, lineCol: screenPos{line: 2, col: 6}, runIndex: 2, towardOrigin: false}, - {runes: 25, lineCol: screenPos{line: 2, col: 7}, runIndex: 2, towardOrigin: false}, - {runes: 26, lineCol: screenPos{line: 2, col: 8}, runIndex: 2, towardOrigin: false}, - {runes: 27, lineCol: screenPos{line: 2, col: 9}, runIndex: 2, towardOrigin: false}, - {runes: 28, lineCol: screenPos{line: 3, col: 0}, runIndex: 0, towardOrigin: true}, - {runes: 29, lineCol: screenPos{line: 3, col: 1}, runIndex: 0, towardOrigin: true}, - {runes: 30, lineCol: screenPos{line: 3, col: 2}, runIndex: 0, towardOrigin: true}, - {runes: 31, lineCol: screenPos{line: 3, col: 3}, runIndex: 0, towardOrigin: true}, - {runes: 32, lineCol: screenPos{line: 3, col: 4}, runIndex: 1, towardOrigin: true}, - {runes: 33, lineCol: screenPos{line: 3, col: 5}, runIndex: 1, towardOrigin: true}, - {runes: 34, lineCol: screenPos{line: 3, col: 6}, runIndex: 1, towardOrigin: true}, - {runes: 35, lineCol: screenPos{line: 4, col: 0}, runIndex: 0, towardOrigin: true}, - {runes: 36, lineCol: screenPos{line: 4, col: 1}, runIndex: 0, towardOrigin: true}, - {runes: 37, lineCol: screenPos{line: 4, col: 2}, runIndex: 0, towardOrigin: true}, - {runes: 38, lineCol: screenPos{line: 4, col: 3}, runIndex: 0, towardOrigin: true}, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - var gi glyphIndex - gi.reset() - for _, g := range tc.glyphs { - gi.Glyph(g) - } - if len(gi.positions) != len(tc.expected) { - t.Errorf("expected %d positions, got %d", len(tc.expected), len(gi.positions)) - } - for i := range min(len(gi.positions), len(tc.expected)) { - actual := gi.positions[i] - expected := tc.expected[i] - if expected.runes != actual.runes { - t.Errorf("position %d: expected runes=%d, got %d", i, expected.runes, actual.runes) - } - if expected.lineCol != actual.lineCol { - t.Errorf("position %d: expected lineCol=%v, got %v", i, expected.lineCol, actual.lineCol) - } - if expected.runIndex != actual.runIndex { - t.Errorf("position %d: expected runIndex=%d, got %d", i, expected.runIndex, actual.runIndex) - } - if expected.towardOrigin != actual.towardOrigin { - t.Errorf("position %d: expected towardOrigin=%v, got %v", i, expected.towardOrigin, actual.towardOrigin) - } - } - printPositions(t, gi.positions) - if t.Failed() { - printGlyphs(t, tc.glyphs) - } - }) - } -} - -func printPositions(t *testing.T, positions []combinedPos) { - t.Helper() - for i, p := range positions { - t.Logf("positions[%2d] = {runes: %2d, line: %2d, col: %2d, x: %5d, y: %3d}", i, p.runes, p.lineCol.line, p.lineCol.col, p.x, p.y) - } -} - -func printGlyphs(t *testing.T, glyphs []text.Glyph) { - t.Helper() - for i, g := range glyphs { - t.Logf("glyphs[%2d] = {ID: 0x%013x, Flags: %4s, Advance: %4d(%6v), Runes: %d, Y: %3d, X: %4d(%6v)} ", i, g.ID, g.Flags, g.Advance, g.Advance, g.Runes, g.Y, g.X, g.X) - } -} - -func TestGraphemeReaderNext(t *testing.T) { - latinDoc := bytes.NewReader([]byte(latinDocument)) - arabicDoc := bytes.NewReader([]byte(arabicDocument)) - emojiDoc := bytes.NewReader([]byte(emojiDocument)) - complexDoc := bytes.NewReader([]byte(complexDocument)) - type testcase struct { - name string - input *bytes.Reader - read func() ([]rune, bool) - } - var pr graphemeReader - for _, tc := range []testcase{ - { - name: "latin", - input: latinDoc, - read: pr.next, - }, - { - name: "arabic", - input: arabicDoc, - read: pr.next, - }, - { - name: "emoji", - input: emojiDoc, - read: pr.next, - }, - { - name: "complex", - input: complexDoc, - read: pr.next, - }, - } { - t.Run(tc.name, func(t *testing.T) { - pr.SetSource(tc.input) - - runes := []rune{} - var paragraph []rune - ok := true - for ok { - paragraph, ok = tc.read() - if ok && len(paragraph) > 0 && paragraph[len(paragraph)-1] != '\n' { - } - for i, r := range paragraph { - if i == len(paragraph)-1 { - if r != '\n' && ok { - t.Error("non-final paragraph does not end with newline") - } - } else if r == '\n' { - t.Errorf("paragraph[%d] contains newline", i) - } - } - runes = append(runes, paragraph...) - } - tc.input.Seek(0, 0) - b, _ := io.ReadAll(tc.input) - asRunes := []rune(string(b)) - if len(asRunes) != len(runes) { - t.Errorf("expected %d runes, got %d", len(asRunes), len(runes)) - } - for i := range max(len(asRunes), len(runes)) { - if i < min(len(asRunes), len(runes)) { - if runes[i] != asRunes[i] { - t.Errorf("expected runes[%d]=%d, got %d", i, asRunes[i], runes[i]) - } - } else if i < len(asRunes) { - t.Errorf("expected runes[%d]=%d, got nothing", i, asRunes[i]) - } else if i < len(runes) { - t.Errorf("expected runes[%d]=nothing, got %d", i, runes[i]) - } - } - }) - } -} - -func TestGraphemeReaderGraphemes(t *testing.T) { - latinDoc := bytes.NewReader([]byte(latinDocument)) - arabicDoc := bytes.NewReader([]byte(arabicDocument)) - emojiDoc := bytes.NewReader([]byte(emojiDocument)) - complexDoc := bytes.NewReader([]byte(complexDocument)) - type testcase struct { - name string - input *bytes.Reader - read func() []int - } - var pr graphemeReader - for _, tc := range []testcase{ - { - name: "latin", - input: latinDoc, - read: pr.Graphemes, - }, - { - name: "arabic", - input: arabicDoc, - read: pr.Graphemes, - }, - { - name: "emoji", - input: emojiDoc, - read: pr.Graphemes, - }, - { - name: "complex", - input: complexDoc, - read: pr.Graphemes, - }, - } { - t.Run(tc.name, func(t *testing.T) { - pr.SetSource(tc.input) - - graphemes := []int{} - for g := tc.read(); len(g) > 0; g = tc.read() { - if len(graphemes) > 0 && g[0] != graphemes[len(graphemes)-1] { - t.Errorf("expected first boundary in new paragraph %d to match final boundary in previous %d", g[0], graphemes[len(graphemes)-1]) - } - if len(graphemes) > 0 { - // Drop duplicated boundary. - g = g[1:] - } - graphemes = append(graphemes, g...) - } - tc.input.Seek(0, 0) - b, _ := io.ReadAll(tc.input) - asRunes := []rune(string(b)) - if len(asRunes)+1 < len(graphemes) { - t.Errorf("expected <= %d graphemes, got %d", len(asRunes)+1, len(graphemes)) - } - for i := range len(graphemes) - 1 { - if graphemes[i] >= graphemes[i+1] { - t.Errorf("graphemes[%d](%d) >= graphemes[%d](%d)", i, graphemes[i], i+1, graphemes[i+1]) - } - } - }) - } -} - -func BenchmarkGraphemeReaderNext(b *testing.B) { - latinDoc := bytes.NewReader([]byte(latinDocument)) - arabicDoc := bytes.NewReader([]byte(arabicDocument)) - emojiDoc := bytes.NewReader([]byte(emojiDocument)) - complexDoc := bytes.NewReader([]byte(complexDocument)) - type testcase struct { - name string - input *bytes.Reader - read func() ([]rune, bool) - } - pr := &graphemeReader{} - for _, tc := range []testcase{ - { - name: "latin", - input: latinDoc, - read: pr.next, - }, - { - name: "arabic", - input: arabicDoc, - read: pr.next, - }, - { - name: "emoji", - input: emojiDoc, - read: pr.next, - }, - { - name: "complex", - input: complexDoc, - read: pr.next, - }, - } { - var paragraph []rune = make([]rune, 4096) - b.Run(tc.name, func(b *testing.B) { - b.ResetTimer() - for b.Loop() { - pr.SetSource(tc.input) - - ok := true - for ok { - paragraph, ok = tc.read() - _ = paragraph - } - _ = paragraph - } - }) - } -} - -func BenchmarkGraphemeReaderGraphemes(b *testing.B) { - latinDoc := bytes.NewReader([]byte(latinDocument)) - arabicDoc := bytes.NewReader([]byte(arabicDocument)) - emojiDoc := bytes.NewReader([]byte(emojiDocument)) - complexDoc := bytes.NewReader([]byte(complexDocument)) - type testcase struct { - name string - input *bytes.Reader - read func() []int - } - pr := &graphemeReader{} - for _, tc := range []testcase{ - { - name: "latin", - input: latinDoc, - read: pr.Graphemes, - }, - { - name: "arabic", - input: arabicDoc, - read: pr.Graphemes, - }, - { - name: "emoji", - input: emojiDoc, - read: pr.Graphemes, - }, - { - name: "complex", - input: complexDoc, - read: pr.Graphemes, - }, - } { - b.Run(tc.name, func(b *testing.B) { - b.ResetTimer() - for b.Loop() { - pr.SetSource(tc.input) - for g := tc.read(); len(g) > 0; g = tc.read() { - _ = g - } - } - }) - } -} diff --git a/gio/widget/label.go b/gio/widget/label.go deleted file mode 100644 index e99d8bc..0000000 --- a/gio/widget/label.go +++ /dev/null @@ -1,226 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - - "golang.org/x/image/math/fixed" -) - -// Label is a widget for laying out and drawing text. Labels are always -// non-interactive text. They cannot be selected or copied. -type Label struct { - // Alignment specifies the text alignment. - Alignment text.Alignment - // MaxLines limits the number of lines. Zero means no limit. - MaxLines int - // Truncator is the text that will be shown at the end of the final - // line if MaxLines is exceeded. Defaults to "…" if empty. - Truncator string - // WrapPolicy configures how displayed text will be broken into lines. - WrapPolicy text.WrapPolicy - // LineHeight controls the distance between the baselines of lines of text. - // If zero, a sensible default will be used. - LineHeight unit.Sp - // LineHeightScale applies a scaling factor to the LineHeight. If zero, a - // sensible default will be used. - LineHeightScale float32 -} - -// Layout the label with the given shaper, font, size, text, and material. -func (l Label) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, txt string, textMaterial op.CallOp) layout.Dimensions { - dims, _ := l.LayoutDetailed(gtx, lt, font, size, txt, textMaterial) - return dims -} - -// TextInfo provides metadata about shaped text. -type TextInfo struct { - // Truncated contains the number of runes of text that are represented by a truncator - // symbol in the text. If zero, there is no truncator symbol. - Truncated int -} - -// Layout the label with the given shaper, font, size, text, and material, returning metadata about the shaped text. -func (l Label) LayoutDetailed(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, txt string, textMaterial op.CallOp) (layout.Dimensions, TextInfo) { - cs := gtx.Constraints - textSize := fixed.I(gtx.Sp(size)) - lineHeight := fixed.I(gtx.Sp(l.LineHeight)) - lt.LayoutString(text.Parameters{ - Font: font, - PxPerEm: textSize, - MaxLines: l.MaxLines, - Truncator: l.Truncator, - Alignment: l.Alignment, - WrapPolicy: l.WrapPolicy, - MaxWidth: cs.Max.X, - MinWidth: cs.Min.X, - Locale: gtx.Locale, - LineHeight: lineHeight, - LineHeightScale: l.LineHeightScale, - }, txt) - m := op.Record(gtx.Ops) - viewport := image.Rectangle{Max: cs.Max} - it := textIterator{ - viewport: viewport, - maxLines: l.MaxLines, - material: textMaterial, - } - semantic.LabelOp(txt).Add(gtx.Ops) - var glyphs [32]text.Glyph - line := glyphs[:0] - for g, ok := lt.NextGlyph(); ok; g, ok = lt.NextGlyph() { - var ok bool - if line, ok = it.paintGlyph(gtx, lt, g, line); !ok { - break - } - } - call := m.Stop() - viewport.Min = viewport.Min.Add(it.padding.Min) - viewport.Max = viewport.Max.Add(it.padding.Max) - clipStack := clip.Rect(viewport).Push(gtx.Ops) - call.Add(gtx.Ops) - dims := layout.Dimensions{Size: it.bounds.Size()} - dims.Size = cs.Constrain(dims.Size) - dims.Baseline = dims.Size.Y - it.baseline - clipStack.Pop() - return dims, TextInfo{Truncated: it.truncated} -} - -// textIterator computes the bounding box of and paints text. -type textIterator struct { - // viewport is the rectangle of document coordinates that the iterator is - // trying to fill with text. - viewport image.Rectangle - // maxLines is the maximum number of text lines that should be displayed. - maxLines int - // material sets the paint material for the text glyphs. If none is provided - // the color of the glyphs is undefined and may change unpredictably if the - // text contains color glyphs. - material op.CallOp - // truncated tracks the count of truncated runes in the text. - truncated int - // linesSeen tracks the quantity of line endings this iterator has seen. - linesSeen int - // lineOff tracks the origin for the glyphs in the current line. - lineOff f32.Point - // padding is the space needed outside of the bounds of the text to ensure no - // part of a glyph is clipped. - padding image.Rectangle - // bounds is the logical bounding box of the text. - bounds image.Rectangle - // visible tracks whether the most recently iterated glyph is visible within - // the viewport. - visible bool - // first tracks whether the iterator has processed a glyph yet. - first bool - // baseline tracks the location of the first line of text's baseline. - baseline int -} - -// processGlyph checks whether the glyph is visible within the iterator's configured -// viewport and (if so) updates the iterator's text dimensions to include the glyph. -func (it *textIterator) processGlyph(g text.Glyph, ok bool) (visibleOrBefore bool) { - if it.maxLines > 0 { - if g.Flags&text.FlagTruncator != 0 && g.Flags&text.FlagClusterBreak != 0 { - // A glyph carrying both of these flags provides the count of truncated runes. - it.truncated = int(g.Runes) - } - if g.Flags&text.FlagLineBreak != 0 { - it.linesSeen++ - } - if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 { - return false - } - } - // Compute the maximum extent to which glyphs overhang on the horizontal - // axis. - if d := g.Bounds.Min.X.Floor(); d < it.padding.Min.X { - // If the distance between the dot and the left edge of this glyph is - // less than the current padding, increase the left padding. - it.padding.Min.X = d - } - if d := (g.Bounds.Max.X - g.Advance).Ceil(); d > it.padding.Max.X { - // If the distance between the dot and the right edge of this glyph - // minus the logical advance of this glyph is greater than the current - // padding, increase the right padding. - it.padding.Max.X = d - } - if d := (g.Bounds.Min.Y + g.Ascent).Floor(); d < it.padding.Min.Y { - // If the distance between the dot and the top of this glyph is greater - // than the ascent of the glyph, increase the top padding. - it.padding.Min.Y = d - } - if d := (g.Bounds.Max.Y - g.Descent).Ceil(); d > it.padding.Max.Y { - // If the distance between the dot and the bottom of this glyph is greater - // than the descent of the glyph, increase the bottom padding. - it.padding.Max.Y = d - } - logicalBounds := image.Rectangle{ - Min: image.Pt(g.X.Floor(), int(g.Y)-g.Ascent.Ceil()), - Max: image.Pt((g.X + g.Advance).Ceil(), int(g.Y)+g.Descent.Ceil()), - } - if !it.first { - it.first = true - it.baseline = int(g.Y) - it.bounds = logicalBounds - } - - above := logicalBounds.Max.Y < it.viewport.Min.Y - below := logicalBounds.Min.Y > it.viewport.Max.Y - left := logicalBounds.Max.X < it.viewport.Min.X - right := logicalBounds.Min.X > it.viewport.Max.X - it.visible = !above && !below && !left && !right - if it.visible { - it.bounds.Min.X = min(it.bounds.Min.X, logicalBounds.Min.X) - it.bounds.Min.Y = min(it.bounds.Min.Y, logicalBounds.Min.Y) - it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X) - it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y) - } - return ok && !below -} - -func fixedToFloat(i fixed.Int26_6) float32 { - return float32(i) / 64.0 -} - -// paintGlyph buffers up and paints text glyphs. It should be invoked iteratively upon each glyph -// until it returns false. The line parameter should be a slice with -// a backing array of sufficient size to buffer multiple glyphs. -// A modified slice will be returned with each invocation, and is -// expected to be passed back in on the following invocation. -// This design is awkward, but prevents the line slice from escaping -// to the heap. -func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) { - visibleOrBefore := it.processGlyph(glyph, true) - if it.visible { - if len(line) == 0 { - it.lineOff = f32.Point{X: fixedToFloat(glyph.X), Y: float32(glyph.Y)}.Sub(layout.FPt(it.viewport.Min)) - } - line = append(line, glyph) - } - if glyph.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 || !visibleOrBefore { - t := op.Affine(f32.AffineId().Offset(it.lineOff)).Push(gtx.Ops) - path := shaper.Shape(line) - outline := clip.Outline{Path: path}.Op().Push(gtx.Ops) - it.material.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - outline.Pop() - if call := shaper.Bitmaps(line); call != (op.CallOp{}) { - call.Add(gtx.Ops) - } - t.Pop() - line = line[:0] - } - return line, visibleOrBefore -} diff --git a/gio/widget/label_test.go b/gio/widget/label_test.go deleted file mode 100644 index cb57d60..0000000 --- a/gio/widget/label_test.go +++ /dev/null @@ -1,229 +0,0 @@ -package widget - -import ( - "image" - "math" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/text" - "golang.org/x/image/math/fixed" -) - -// TestGlyphIterator ensures that the glyph iterator computes correct bounding -// boxes and baselines for a variety of glyph sequences. -func TestGlyphIterator(t *testing.T) { - fontSize := 16 - stdAscent := fixed.I(fontSize) - stdDescent := fixed.I(4) - stdLineHeight := stdAscent + stdDescent - type testcase struct { - name string - str string - maxWidth int - maxLines int - viewport image.Rectangle - expectedDims image.Rectangle - expectedBaseline int - stopAtGlyph int - } - for _, tc := range []testcase{ - { - name: "empty string", - str: "", - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, - expectedDims: image.Rectangle{ - Max: image.Point{X: 0, Y: stdLineHeight.Round()}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 0, - }, - { - name: "simple", - str: "MMM", - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, - expectedDims: image.Rectangle{ - Max: image.Point{X: 40, Y: stdLineHeight.Round()}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 2, - }, - { - name: "simple clipped horizontally", - str: "MMM", - viewport: image.Rectangle{Max: image.Pt(20, math.MaxInt)}, - // The dimensions should only include the first two glyphs. - expectedDims: image.Rectangle{ - Max: image.Point{X: 27, Y: stdLineHeight.Round()}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 2, - }, - { - name: "simple clipped vertically", - str: "M\nM\nM\nM", - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, 2*stdLineHeight.Floor()-3)}, - // The dimensions should only include the first two lines. - expectedDims: image.Rectangle{ - Max: image.Point{X: 14, Y: 39}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 4, - }, - { - name: "simple truncated", - str: "mmm", - maxLines: 1, - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, - // This truncation should have no effect because the text is already one line. - expectedDims: image.Rectangle{ - Max: image.Point{X: 40, Y: stdLineHeight.Round()}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 2, - }, - { - name: "whitespace", - str: " ", - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, - expectedDims: image.Rectangle{ - Max: image.Point{X: 14, Y: stdLineHeight.Round()}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 2, - }, - { - name: "multi-line with hard newline", - str: "你\n好", - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, - expectedDims: image.Rectangle{ - Max: image.Point{X: 12, Y: 39}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 3, - }, - { - name: "multi-line with soft newline", - str: "你好", // UAX#14 allows line breaking between these characters. - maxWidth: fontSize, - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, - expectedDims: image.Rectangle{ - Max: image.Point{X: 12, Y: 39}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 2, - }, - { - name: "trailing hard newline", - str: "m\n", - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, - // We expect the dimensions to account for two vertical lines because of the - // newline at the end. - expectedDims: image.Rectangle{ - Max: image.Point{X: 14, Y: 39}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 1, - }, - { - name: "truncated trailing hard newline", - str: "m\n", - maxLines: 1, - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, - // We expect the dimensions to reflect only a single line despite the newline - // at the end. - expectedDims: image.Rectangle{ - Max: image.Point{X: 14, Y: 20}, - }, - expectedBaseline: fontSize, - stopAtGlyph: 1, - }, - } { - t.Run(tc.name, func(t *testing.T) { - maxWidth := 200 - if tc.maxWidth != 0 { - maxWidth = tc.maxWidth - } - glyphs := getGlyphs(16, 0, maxWidth, text.Start, tc.str) - it := textIterator{viewport: tc.viewport, maxLines: tc.maxLines} - for i, g := range glyphs { - ok := it.processGlyph(g, true) - if !ok && i != tc.stopAtGlyph { - t.Errorf("expected iterator to stop at glyph %d, stopped at %d", tc.stopAtGlyph, i) - } - if !ok { - break - } - } - if it.bounds != tc.expectedDims { - t.Errorf("expected bounds %#+v, got %#+v", tc.expectedDims, it.bounds) - } - if it.baseline != tc.expectedBaseline { - t.Errorf("expected baseline %d, got %d", tc.expectedBaseline, it.baseline) - } - }) - } -} - -// TestGlyphIteratorPadding ensures that the glyph iterator computes correct padding -// around glyphs with unusual bounding boxes. -func TestGlyphIteratorPadding(t *testing.T) { - type testcase struct { - name string - glyph text.Glyph - viewport image.Rectangle - expectedDims image.Rectangle - expectedPadding image.Rectangle - expectedBaseline int - } - for _, tc := range []testcase{ - { - name: "simple", - glyph: text.Glyph{ - X: 0, - Y: 50, - Advance: fixed.I(50), - Ascent: fixed.I(50), - Descent: fixed.I(50), - Bounds: fixed.Rectangle26_6{ - Min: fixed.Point26_6{ - X: fixed.I(-5), - Y: fixed.I(-56), - }, - Max: fixed.Point26_6{ - X: fixed.I(57), - Y: fixed.I(58), - }, - }, - }, - viewport: image.Rectangle{Max: image.Pt(math.MaxInt, math.MaxInt)}, - expectedDims: image.Rectangle{ - Max: image.Point{X: 50, Y: 100}, - }, - expectedBaseline: 50, - expectedPadding: image.Rectangle{ - Min: image.Point{ - X: -5, - Y: -6, - }, - Max: image.Point{ - X: 7, - Y: 8, - }, - }, - }, - } { - t.Run(tc.name, func(t *testing.T) { - it := textIterator{viewport: tc.viewport} - it.processGlyph(tc.glyph, true) - if it.bounds != tc.expectedDims { - t.Errorf("expected bounds %#+v, got %#+v", tc.expectedDims, it.bounds) - } - if it.baseline != tc.expectedBaseline { - t.Errorf("expected baseline %d, got %d", tc.expectedBaseline, it.baseline) - } - if it.padding != tc.expectedPadding { - t.Errorf("expected padding %d, got %d", tc.expectedPadding, it.padding) - } - }) - } -} diff --git a/gio/widget/list.go b/gio/widget/list.go deleted file mode 100644 index 97bb2db..0000000 --- a/gio/widget/list.go +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget - -import ( - "image" - - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" -) - -// Scrollbar holds the persistent state for an area that can -// display a scrollbar. In particular, it tracks the position of a -// viewport along a one-dimensional region of content. The viewport's -// position can be adjusted by drag operations along the display area, -// or by clicks within the display area. -// -// Scrollbar additionally detects when a scroll indicator region is -// hovered. -type Scrollbar struct { - track, indicator gesture.Click - drag gesture.Drag - delta float32 - - dragging bool - oldDragPos float32 -} - -// Update updates the internal state of the scrollbar based on events -// since the previous call to Update. The provided axis will be used to -// normalize input event coordinates and constraints into an axis- -// independent format. viewportStart is the position of the beginning -// of the scrollable viewport relative to the underlying content expressed -// as a value in the range [0,1]. viewportEnd is the position of the end -// of the viewport relative to the underlying content, also expressed -// as a value in the range [0,1]. For example, if viewportStart is 0.25 -// and viewportEnd is .5, the viewport described by the scrollbar is -// currently showing the second quarter of the underlying content. -func (s *Scrollbar) Update(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) { - // Calculate the length of the major axis of the scrollbar. This is - // the length of the track within which pointer events occur, and is - // used to scale those interactions. - trackHeight := float32(axis.Convert(gtx.Constraints.Max).X) - s.delta = 0 - - centerOnClick := func(normalizedPos float32) { - // When the user clicks on the scrollbar we center on that point, respecting the limits of the beginning and end - // of the scrollbar. - // - // Centering gives a consistent experience whether the user clicks above or below the indicator. - target := normalizedPos - (viewportEnd-viewportStart)/2 - s.delta += target - viewportStart - if s.delta < -viewportStart { - s.delta = -viewportStart - } else if s.delta > 1-viewportEnd { - s.delta = 1 - viewportEnd - } - } - - // Jump to a click in the track. - for { - event, ok := s.track.Update(gtx.Source) - if !ok { - break - } - if event.Kind != gesture.KindClick || - event.Modifiers != key.Modifiers(0) || - event.NumClicks > 1 { - continue - } - pos := axis.Convert(image.Point{ - X: int(event.Position.X), - Y: int(event.Position.Y), - }) - normalizedPos := float32(pos.X) / trackHeight - // Clicking on the indicator should not jump to that position on the track. The user might've just intended to - // drag and changed their mind. - if !(normalizedPos >= viewportStart && normalizedPos <= viewportEnd) { - centerOnClick(normalizedPos) - } - } - - // Offset to account for any drags. - for { - event, ok := s.drag.Update(gtx.Metric, gtx.Source, gesture.Axis(axis)) - if !ok { - break - } - switch event.Kind { - case pointer.Drag: - case pointer.Release, pointer.Cancel: - s.dragging = false - continue - default: - continue - } - dragOffset := axis.FConvert(event.Position).X - // The user can drag outside of the constraints, or even the window. Limit dragging to within the scrollbar. - if dragOffset < 0 { - dragOffset = 0 - } else if dragOffset > trackHeight { - dragOffset = trackHeight - } - normalizedDragOffset := dragOffset / trackHeight - - if !s.dragging { - s.dragging = true - s.oldDragPos = normalizedDragOffset - - if normalizedDragOffset < viewportStart || normalizedDragOffset > viewportEnd { - // The user started dragging somewhere on the track that isn't covered by the indicator. Consider this a - // click in addition to a drag and jump to the clicked point. - // - // TODO(dh): this isn't perfect. We only get the pointer.Drag event once the user has actually dragged, - // which means that if the user presses the mouse button and neither releases it nor drags it, nothing - // will happen. - pos := axis.Convert(image.Point{ - X: int(event.Position.X), - Y: int(event.Position.Y), - }) - normalizedPos := float32(pos.X) / trackHeight - centerOnClick(normalizedPos) - } - } else { - s.delta += normalizedDragOffset - s.oldDragPos - - if viewportStart+s.delta < 0 { - // Adjust normalizedDragOffset - and thus the future s.oldDragPos - so that futile dragging up has to be - // countered with dragging down again. Otherwise, dragging up would have no effect, but dragging down would - // immediately start scrolling. We want the user to undo their ineffective drag first. - normalizedDragOffset -= viewportStart + s.delta - // Limit s.delta to the maximum amount scrollable - s.delta = -viewportStart - } else if viewportEnd+s.delta > 1 { - normalizedDragOffset += (1 - viewportEnd) - s.delta - s.delta = 1 - viewportEnd - } - s.oldDragPos = normalizedDragOffset - } - } - - // Process events from the indicator so that hover is - // detected properly. - for { - if _, ok := s.indicator.Update(gtx.Source); !ok { - break - } - } -} - -// AddTrack configures the track click listener for the scrollbar to use -// the current clip area. -func (s *Scrollbar) AddTrack(ops *op.Ops) { - s.track.Add(ops) -} - -// AddIndicator configures the indicator click listener for the scrollbar to use -// the current clip area. -func (s *Scrollbar) AddIndicator(ops *op.Ops) { - s.indicator.Add(ops) -} - -// AddDrag configures the drag listener for the scrollbar to use -// the current clip area. -func (s *Scrollbar) AddDrag(ops *op.Ops) { - s.drag.Add(ops) -} - -// IndicatorHovered reports whether the scroll indicator is currently being -// hovered by the pointer. -func (s *Scrollbar) IndicatorHovered() bool { - return s.indicator.Hovered() -} - -// TrackHovered reports whether the scroll track is being hovered by the -// pointer. -func (s *Scrollbar) TrackHovered() bool { - return s.track.Hovered() -} - -// ScrollDistance returns the normalized distance that the scrollbar -// moved during the last call to Layout as a value in the range [-1,1]. -func (s *Scrollbar) ScrollDistance() float32 { - return s.delta -} - -// Dragging reports whether the user is currently performing a drag gesture -// on the indicator. Note that this can return false while ScrollDistance is nonzero -// if the user scrolls using a different control than the scrollbar (like a mouse -// wheel). -func (s *Scrollbar) Dragging() bool { - return s.dragging -} - -// List holds the persistent state for a layout.List that has a -// scrollbar attached. -type List struct { - Scrollbar - layout.List -} diff --git a/gio/widget/material/button.go b/gio/widget/material/button.go deleted file mode 100644 index c8b3dc2..0000000 --- a/gio/widget/material/button.go +++ /dev/null @@ -1,297 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image" - "image/color" - "math" - - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -type ButtonStyle struct { - Text string - // Color is the text color. - Color color.NRGBA - Font font.Font - TextSize unit.Sp - Background color.NRGBA - CornerRadius unit.Dp - Inset layout.Inset - Button *widget.Clickable - shaper *text.Shaper -} - -type ButtonLayoutStyle struct { - Background color.NRGBA - CornerRadius unit.Dp - Button *widget.Clickable -} - -type IconButtonStyle struct { - Background color.NRGBA - // Color is the icon color. - Color color.NRGBA - Icon *widget.Icon - // Size is the icon size. - Size unit.Dp - Inset layout.Inset - Button *widget.Clickable - Description string -} - -func Button(th *Theme, button *widget.Clickable, txt string) ButtonStyle { - b := ButtonStyle{ - Text: txt, - Color: th.Palette.ContrastFg, - CornerRadius: 4, - Background: th.Palette.ContrastBg, - TextSize: th.TextSize * 14.0 / 16.0, - Inset: layout.Inset{ - Top: 10, Bottom: 10, - Left: 12, Right: 12, - }, - Button: button, - shaper: th.Shaper, - } - b.Font.Typeface = th.Face - return b -} - -func ButtonLayout(th *Theme, button *widget.Clickable) ButtonLayoutStyle { - return ButtonLayoutStyle{ - Button: button, - Background: th.Palette.ContrastBg, - CornerRadius: 4, - } -} - -func IconButton(th *Theme, button *widget.Clickable, icon *widget.Icon, description string) IconButtonStyle { - return IconButtonStyle{ - Background: th.Palette.ContrastBg, - Color: th.Palette.ContrastFg, - Icon: icon, - Size: 24, - Inset: layout.UniformInset(12), - Button: button, - Description: description, - } -} - -// Clickable lays out a rectangular clickable widget without further -// decoration. -func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions { - return button.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - semantic.Button.Add(gtx.Ops) - return layout.Background{}.Layout(gtx, - func(gtx layout.Context) layout.Dimensions { - defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop() - if button.Hovered() || gtx.Focused(button) { - paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{})) - } - for _, c := range button.History() { - drawInk(gtx, c) - } - return layout.Dimensions{Size: gtx.Constraints.Min} - }, - w, - ) - }) -} - -func (b ButtonStyle) Layout(gtx layout.Context) layout.Dimensions { - return ButtonLayoutStyle{ - Background: b.Background, - CornerRadius: b.CornerRadius, - Button: b.Button, - }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - colMacro := op.Record(gtx.Ops) - paint.ColorOp{Color: b.Color}.Add(gtx.Ops) - return widget.Label{Alignment: text.Middle}.Layout(gtx, b.shaper, b.Font, b.TextSize, b.Text, colMacro.Stop()) - }) - }) -} - -func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { - min := gtx.Constraints.Min - return b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - semantic.Button.Add(gtx.Ops) - return layout.Background{}.Layout(gtx, - func(gtx layout.Context) layout.Dimensions { - rr := gtx.Dp(b.CornerRadius) - defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop() - background := b.Background - switch { - case !gtx.Enabled(): - background = f32color.Disabled(b.Background) - case b.Button.Hovered() || gtx.Focused(b.Button): - background = f32color.Hovered(b.Background) - } - paint.Fill(gtx.Ops, background) - for _, c := range b.Button.History() { - drawInk(gtx, c) - } - return layout.Dimensions{Size: gtx.Constraints.Min} - }, - func(gtx layout.Context) layout.Dimensions { - gtx.Constraints.Min = min - return layout.Center.Layout(gtx, w) - }, - ) - }) -} - -func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions { - m := op.Record(gtx.Ops) - dims := b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - semantic.Button.Add(gtx.Ops) - if d := b.Description; d != "" { - semantic.DescriptionOp(b.Description).Add(gtx.Ops) - } - return layout.Background{}.Layout(gtx, - func(gtx layout.Context) layout.Dimensions { - rr := (gtx.Constraints.Min.X + gtx.Constraints.Min.Y) / 4 - defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop() - background := b.Background - switch { - case !gtx.Enabled(): - background = f32color.Disabled(b.Background) - case b.Button.Hovered() || gtx.Focused(b.Button): - background = f32color.Hovered(b.Background) - } - paint.Fill(gtx.Ops, background) - for _, c := range b.Button.History() { - drawInk(gtx, c) - } - return layout.Dimensions{Size: gtx.Constraints.Min} - }, - func(gtx layout.Context) layout.Dimensions { - return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - size := gtx.Dp(b.Size) - if b.Icon != nil { - gtx.Constraints.Min = image.Point{X: size} - b.Icon.Layout(gtx, b.Color) - } - return layout.Dimensions{ - Size: image.Point{X: size, Y: size}, - } - }) - }, - ) - }) - c := m.Stop() - bounds := image.Rectangle{Max: dims.Size} - defer clip.Ellipse(bounds).Push(gtx.Ops).Pop() - c.Add(gtx.Ops) - return dims -} - -func drawInk(gtx layout.Context, c widget.Press) { - // duration is the number of seconds for the - // completed animation: expand while fading in, then - // out. - const ( - expandDuration = float32(0.5) - fadeDuration = float32(0.9) - ) - - now := gtx.Now - - t := float32(now.Sub(c.Start).Seconds()) - - end := c.End - if end.IsZero() { - // If the press hasn't ended, don't fade-out. - end = now - } - - endt := float32(end.Sub(c.Start).Seconds()) - - // Compute the fade-in/out position in [0;1]. - var alphat float32 - { - var haste float32 - if c.Cancelled { - // If the press was cancelled before the inkwell - // was fully faded in, fast forward the animation - // to match the fade-out. - if h := 0.5 - endt/fadeDuration; h > 0 { - haste = h - } - } - // Fade in. - half1 := t/fadeDuration + haste - if half1 > 0.5 { - half1 = 0.5 - } - - // Fade out. - half2 := float32(now.Sub(end).Seconds()) - half2 /= fadeDuration - half2 += haste - if half2 > 0.5 { - // Too old. - return - } - - alphat = half1 + half2 - } - - // Compute the expand position in [0;1]. - sizet := t - if c.Cancelled { - // Freeze expansion of cancelled presses. - sizet = endt - } - sizet /= expandDuration - - // Animate only ended presses, and presses that are fading in. - if !c.End.IsZero() || sizet <= 1.0 { - gtx.Execute(op.InvalidateCmd{}) - } - - if sizet > 1.0 { - sizet = 1.0 - } - - if alphat > .5 { - // Start fadeout after half the animation. - alphat = 1.0 - alphat - } - // Twice the speed to attain fully faded in at 0.5. - t2 := alphat * 2 - // Beziér ease-in curve. - alphaBezier := t2 * t2 * (3.0 - 2.0*t2) - sizeBezier := sizet * sizet * (3.0 - 2.0*sizet) - size := gtx.Constraints.Min.X - if h := gtx.Constraints.Min.Y; h > size { - size = h - } - // Cover the entire constraints min rectangle and - // apply curve values to size and color. - size = int(float32(size) * 2 * float32(math.Sqrt(2)) * sizeBezier) - alpha := 0.7 * alphaBezier - const col = 0.8 - ba, bc := byte(alpha*0xff), byte(col*0xff) - rgba := f32color.MulAlpha(color.NRGBA{A: 0xff, R: bc, G: bc, B: bc}, ba) - ink := paint.ColorOp{Color: rgba} - ink.Add(gtx.Ops) - rr := size / 2 - defer op.Offset(c.Position.Add(image.Point{ - X: -rr, - Y: -rr, - })).Push(gtx.Ops).Pop() - defer clip.UniformRRect(image.Rectangle{Max: image.Pt(size, size)}, rr).Push(gtx.Ops).Pop() - paint.PaintOp{}.Add(gtx.Ops) -} diff --git a/gio/widget/material/checkable.go b/gio/widget/material/checkable.go deleted file mode 100644 index 3f38751..0000000 --- a/gio/widget/material/checkable.go +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image" - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -type checkable struct { - Label string - Color color.NRGBA - Font font.Font - TextSize unit.Sp - IconColor color.NRGBA - Size unit.Dp - shaper *text.Shaper - checkedStateIcon *widget.Icon - uncheckedStateIcon *widget.Icon -} - -func (c *checkable) layout(gtx layout.Context, checked, hovered bool) layout.Dimensions { - var icon *widget.Icon - if checked { - icon = c.checkedStateIcon - } else { - icon = c.uncheckedStateIcon - } - - dims := layout.Flex{Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.Stack{Alignment: layout.Center}.Layout(gtx, - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - size := gtx.Dp(c.Size) * 4 / 3 - dims := layout.Dimensions{ - Size: image.Point{X: size, Y: size}, - } - if !hovered { - return dims - } - - background := f32color.MulAlpha(c.IconColor, 70) - - b := image.Rectangle{Max: image.Pt(size, size)} - paint.FillShape(gtx.Ops, background, clip.Ellipse(b).Op(gtx.Ops)) - - return dims - }), - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - size := gtx.Dp(c.Size) - col := c.IconColor - if !gtx.Enabled() { - col = f32color.Disabled(col) - } - gtx.Constraints.Min = image.Point{X: size} - icon.Layout(gtx, col) - return layout.Dimensions{ - Size: image.Point{X: size, Y: size}, - } - }) - }), - ) - }), - - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(2).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - colMacro := op.Record(gtx.Ops) - paint.ColorOp{Color: c.Color}.Add(gtx.Ops) - return widget.Label{}.Layout(gtx, c.shaper, c.Font, c.TextSize, c.Label, colMacro.Stop()) - }) - }), - ) - return dims -} diff --git a/gio/widget/material/checkbox.go b/gio/widget/material/checkbox.go deleted file mode 100644 index a2a41de..0000000 --- a/gio/widget/material/checkbox.go +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -type CheckBoxStyle struct { - checkable - CheckBox *widget.Bool -} - -func CheckBox(th *Theme, checkBox *widget.Bool, label string) CheckBoxStyle { - c := CheckBoxStyle{ - CheckBox: checkBox, - checkable: checkable{ - Label: label, - Color: th.Palette.Fg, - IconColor: th.Palette.ContrastBg, - TextSize: th.TextSize * 14.0 / 16.0, - Size: 26, - shaper: th.Shaper, - checkedStateIcon: th.Icon.CheckBoxChecked, - uncheckedStateIcon: th.Icon.CheckBoxUnchecked, - }, - } - c.checkable.Font.Typeface = th.Face - return c -} - -// Layout updates the checkBox and displays it. -func (c CheckBoxStyle) Layout(gtx layout.Context) layout.Dimensions { - return c.CheckBox.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - semantic.CheckBox.Add(gtx.Ops) - return c.layout(gtx, c.CheckBox.Value, c.CheckBox.Hovered() || gtx.Focused(c.CheckBox)) - }) -} diff --git a/gio/widget/material/decorations.go b/gio/widget/material/decorations.go deleted file mode 100644 index d1a912d..0000000 --- a/gio/widget/material/decorations.go +++ /dev/null @@ -1,209 +0,0 @@ -package material - -import ( - "image" - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -// DecorationsStyle provides the style elements for Decorations. -type DecorationsStyle struct { - Decorations *widget.Decorations - Actions system.Action - Title LabelStyle - Background color.NRGBA - Foreground color.NRGBA -} - -// Decorations returns the style to decorate a window. -func Decorations(th *Theme, deco *widget.Decorations, actions system.Action, title string) DecorationsStyle { - titleStyle := Body1(th, title) - titleStyle.Color = th.Palette.ContrastFg - return DecorationsStyle{ - Decorations: deco, - Actions: actions, - Title: titleStyle, - Background: th.Palette.ContrastBg, - Foreground: th.Palette.ContrastFg, - } -} - -// Layout a window with its title and action buttons. -func (d DecorationsStyle) Layout(gtx layout.Context) layout.Dimensions { - rec := op.Record(gtx.Ops) - dims := d.layoutDecorations(gtx) - decos := rec.Stop() - r := clip.Rect{Max: dims.Size} - paint.FillShape(gtx.Ops, d.Background, r.Op()) - decos.Add(gtx.Ops) - return dims -} - -func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimensions { - gtx.Constraints.Min.Y = 0 - inset := layout.UniformInset(10) - return layout.Flex{ - Axis: layout.Horizontal, - Alignment: layout.Middle, - Spacing: layout.SpaceBetween, - }.Layout(gtx, - layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { - return d.Decorations.LayoutMove(gtx, func(gtx layout.Context) layout.Dimensions { - return inset.Layout(gtx, d.Title.Layout) - }) - }), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - // Remove the unmaximize action as it is taken care of by maximize. - actions := d.Actions &^ system.ActionUnmaximize - var size image.Point - for a := system.Action(1); actions != 0; a <<= 1 { - if a&actions == 0 { - continue - } - actions &^= a - var w layout.Widget - switch a { - case system.ActionMinimize: - w = minimizeWindow - case system.ActionMaximize: - if d.Decorations.Maximized { - w = maximizedWindow - } else { - w = maximizeWindow - } - case system.ActionClose: - w = closeWindow - default: - continue - } - cl := d.Decorations.Clickable(a) - dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - semantic.Button.Add(gtx.Ops) - return layout.Background{}.Layout(gtx, - func(gtx layout.Context) layout.Dimensions { - defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop() - for _, c := range cl.History() { - drawInk(gtx, c) - } - return layout.Dimensions{Size: gtx.Constraints.Min} - }, - func(gtx layout.Context) layout.Dimensions { - paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops) - return inset.Layout(gtx, w) - }, - ) - }) - size.X += dims.Size.X - if size.Y < dims.Size.Y { - size.Y = dims.Size.Y - } - op.Offset(image.Pt(dims.Size.X, 0)).Add(gtx.Ops) - } - return layout.Dimensions{Size: size} - }), - ) -} - -const ( - winIconSize = unit.Dp(20) - winIconMargin = unit.Dp(4) - winIconStroke = unit.Dp(2) -) - -// minimizeWindows draws a line icon representing the minimize action. -func minimizeWindow(gtx layout.Context) layout.Dimensions { - size := gtx.Dp(winIconSize) - size32 := float32(size) - margin := float32(gtx.Dp(winIconMargin)) - width := float32(gtx.Dp(winIconStroke)) - var p clip.Path - p.Begin(gtx.Ops) - p.MoveTo(f32.Point{X: margin, Y: size32 - margin}) - p.LineTo(f32.Point{X: size32 - 2*margin, Y: size32 - margin}) - st := clip.Stroke{ - Path: p.End(), - Width: width, - }.Op().Push(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - st.Pop() - return layout.Dimensions{Size: image.Pt(size, size)} -} - -// maximizeWindow draws a rectangle representing the maximize action. -func maximizeWindow(gtx layout.Context) layout.Dimensions { - size := gtx.Dp(winIconSize) - margin := gtx.Dp(winIconMargin) - width := gtx.Dp(winIconStroke) - r := clip.RRect{ - Rect: image.Rect(margin, margin, size-margin, size-margin), - } - st := clip.Stroke{ - Path: r.Path(gtx.Ops), - Width: float32(width), - }.Op().Push(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - st.Pop() - r.Rect.Max = image.Pt(size-margin, 2*margin) - st = clip.Outline{ - Path: r.Path(gtx.Ops), - }.Op().Push(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - st.Pop() - return layout.Dimensions{Size: image.Pt(size, size)} -} - -// maximizedWindow draws interleaved rectangles representing the un-maximize action. -func maximizedWindow(gtx layout.Context) layout.Dimensions { - size := gtx.Dp(winIconSize) - margin := gtx.Dp(winIconMargin) - width := gtx.Dp(winIconStroke) - r := clip.RRect{ - Rect: image.Rect(margin, margin, size-2*margin, size-2*margin), - } - st := clip.Stroke{ - Path: r.Path(gtx.Ops), - Width: float32(width), - }.Op().Push(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - st.Pop() - r = clip.RRect{ - Rect: image.Rect(2*margin, 2*margin, size-margin, size-margin), - } - st = clip.Stroke{ - Path: r.Path(gtx.Ops), - Width: float32(width), - }.Op().Push(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - st.Pop() - return layout.Dimensions{Size: image.Pt(size, size)} -} - -// closeWindow draws a cross representing the close action. -func closeWindow(gtx layout.Context) layout.Dimensions { - size := gtx.Dp(winIconSize) - size32 := float32(size) - margin := float32(gtx.Dp(winIconMargin)) - width := float32(gtx.Dp(winIconStroke)) - var p clip.Path - p.Begin(gtx.Ops) - p.MoveTo(f32.Point{X: margin, Y: margin}) - p.LineTo(f32.Point{X: size32 - margin, Y: size32 - margin}) - p.MoveTo(f32.Point{X: size32 - margin, Y: margin}) - p.LineTo(f32.Point{X: margin, Y: size32 - margin}) - st := clip.Stroke{ - Path: p.End(), - Width: width, - }.Op().Push(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - st.Pop() - return layout.Dimensions{Size: image.Pt(size, size)} -} diff --git a/gio/widget/material/doc.go b/gio/widget/material/doc.go deleted file mode 100644 index 5b52726..0000000 --- a/gio/widget/material/doc.go +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -// Package material implements the Material design. -// -// To maximize reusability and visual flexibility, user interface controls are -// split into two parts: the stateful widget and the stateless drawing of it. -// -// For example, widget.Clickable encapsulates the state and event -// handling of all clickable areas, while the Theme is responsible to -// draw a specific area, for example a button. -// -// This snippet defines a button that prints a message when clicked: -// -// var gtx layout.Context -// button := new(widget.Clickable) -// -// for button.Clicked(gtx) { -// fmt.Println("Clicked!") -// } -// -// Use a Theme to draw the button: -// -// theme := material.NewTheme(...) -// -// material.Button(theme, button, "Click me!").Layout(gtx) -// -// # Customization -// -// Quite often, a program needs to customize the theme-provided defaults. Several -// options are available, depending on the nature of the change. -// -// Mandatory parameters: Some parameters are not part of the widget state but -// have no obvious default. In the program above, the button text is a -// parameter to the Theme.Button method. -// -// Theme-global parameters: For changing the look of all widgets drawn with a -// particular theme, adjust the `Theme` fields: -// -// theme.Palette.Fg = color.NRGBA{...} -// -// Widget-local parameters: For changing the look of a particular widget, -// adjust the widget specific theme object: -// -// btn := material.Button(theme, button, "Click me!") -// btn.Font.Style = text.Italic -// btn.Layout(gtx) -// -// Widget variants: A widget can have several distinct representations even -// though the underlying state is the same. A widget.Clickable can be drawn as a -// round icon button: -// -// icon := widget.NewIcon(...) -// -// material.IconButton(theme, button, icon, "Click me!").Layout(gtx) -// -// Specialized widgets: Theme both define a generic Label method -// that takes a text size, and specialized methods for standard text -// sizes such as Theme.H1 and Theme.Body2. -package material diff --git a/gio/widget/material/editor.go b/gio/widget/material/editor.go deleted file mode 100644 index 8c644dd..0000000 --- a/gio/widget/material/editor.go +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -type EditorStyle struct { - Font font.Font - // LineHeight controls the distance between the baselines of lines of text. - // If zero, a sensible default will be used. - LineHeight unit.Sp - // LineHeightScale applies a scaling factor to the LineHeight. If zero, a - // sensible default will be used. - LineHeightScale float32 - TextSize unit.Sp - // Color is the text color. - Color color.NRGBA - // Hint contains the text displayed when the editor is empty. - Hint string - // HintColor is the color of hint text. - HintColor color.NRGBA - // SelectionColor is the color of the background for selected text. - SelectionColor color.NRGBA - Editor *widget.Editor - - shaper *text.Shaper -} - -func Editor(th *Theme, editor *widget.Editor, hint string) EditorStyle { - return EditorStyle{ - Editor: editor, - Font: font.Font{ - Typeface: th.Face, - }, - TextSize: th.TextSize, - Color: th.Palette.Fg, - shaper: th.Shaper, - Hint: hint, - HintColor: f32color.MulAlpha(th.Palette.Fg, 0xbb), - SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60), - } -} - -func (e EditorStyle) Layout(gtx layout.Context) layout.Dimensions { - // Choose colors. - textColorMacro := op.Record(gtx.Ops) - paint.ColorOp{Color: e.Color}.Add(gtx.Ops) - textColor := textColorMacro.Stop() - hintColorMacro := op.Record(gtx.Ops) - paint.ColorOp{Color: e.HintColor}.Add(gtx.Ops) - hintColor := hintColorMacro.Stop() - selectionColorMacro := op.Record(gtx.Ops) - paint.ColorOp{Color: blendDisabledColor(!gtx.Enabled(), e.SelectionColor)}.Add(gtx.Ops) - selectionColor := selectionColorMacro.Stop() - - var maxlines int - if e.Editor.SingleLine { - maxlines = 1 - } - - macro := op.Record(gtx.Ops) - tl := widget.Label{ - Alignment: e.Editor.Alignment, - MaxLines: maxlines, - LineHeight: e.LineHeight, - LineHeightScale: e.LineHeightScale, - } - dims := tl.Layout(gtx, e.shaper, e.Font, e.TextSize, e.Hint, hintColor) - call := macro.Stop() - - if w := dims.Size.X; gtx.Constraints.Min.X < w { - gtx.Constraints.Min.X = w - } - if h := dims.Size.Y; gtx.Constraints.Min.Y < h { - gtx.Constraints.Min.Y = h - } - e.Editor.LineHeight = e.LineHeight - e.Editor.LineHeightScale = e.LineHeightScale - dims = e.Editor.Layout(gtx, e.shaper, e.Font, e.TextSize, textColor, selectionColor) - if e.Editor.Len() == 0 { - call.Add(gtx.Ops) - } - return dims -} - -func blendDisabledColor(disabled bool, c color.NRGBA) color.NRGBA { - if disabled { - return f32color.Disabled(c) - } - return c -} diff --git a/gio/widget/material/label.go b/gio/widget/material/label.go deleted file mode 100644 index 078c2ed..0000000 --- a/gio/widget/material/label.go +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -// LabelStyle configures the presentation of text. If the State field is set, the -// label will be laid out as interactive (able to be selected and copied). Otherwise, -// the label will be non-interactive. -type LabelStyle struct { - // Face defines the text style. - Font font.Font - // Color is the text color. - Color color.NRGBA - // SelectionColor is the color of the background for selected text. - SelectionColor color.NRGBA - // Alignment specify the text alignment. - Alignment text.Alignment - // MaxLines limits the number of lines. Zero means no limit. - MaxLines int - // WrapPolicy configures how displayed text will be broken into lines. - WrapPolicy text.WrapPolicy - // Truncator is the text that will be shown at the end of the final - // line if MaxLines is exceeded. Defaults to "…" if empty. - Truncator string - // Text is the content displayed by the label. - Text string - // TextSize determines the size of the text glyphs. - TextSize unit.Sp - // LineHeight controls the distance between the baselines of lines of text. - // If zero, a sensible default will be used. - LineHeight unit.Sp - // LineHeightScale applies a scaling factor to the LineHeight. If zero, a - // sensible default will be used. - LineHeightScale float32 - - // Shaper is the text shaper used to display this labe. This field is automatically - // set using by all constructor functions. If constructing a LabelStyle literal, you - // must provide a Shaper or displaying text will panic. - Shaper *text.Shaper - // State provides text selection state for the label. If not set, the label cannot - // be selected or copied interactively. - State *widget.Selectable -} - -func H1(th *Theme, txt string) LabelStyle { - label := Label(th, th.TextSize*96.0/16.0, txt) - label.Font.Weight = font.Light - return label -} - -func H2(th *Theme, txt string) LabelStyle { - label := Label(th, th.TextSize*60.0/16.0, txt) - label.Font.Weight = font.Light - return label -} - -func H3(th *Theme, txt string) LabelStyle { - return Label(th, th.TextSize*48.0/16.0, txt) -} - -func H4(th *Theme, txt string) LabelStyle { - return Label(th, th.TextSize*34.0/16.0, txt) -} - -func H5(th *Theme, txt string) LabelStyle { - return Label(th, th.TextSize*24.0/16.0, txt) -} - -func H6(th *Theme, txt string) LabelStyle { - label := Label(th, th.TextSize*20.0/16.0, txt) - label.Font.Weight = font.Medium - return label -} - -func Subtitle1(th *Theme, txt string) LabelStyle { - return Label(th, th.TextSize*16.0/16.0, txt) -} - -func Subtitle2(th *Theme, txt string) LabelStyle { - label := Label(th, th.TextSize*14.0/16.0, txt) - label.Font.Weight = font.Medium - return label -} - -func Body1(th *Theme, txt string) LabelStyle { - return Label(th, th.TextSize, txt) -} - -func Body2(th *Theme, txt string) LabelStyle { - return Label(th, th.TextSize*14.0/16.0, txt) -} - -func Caption(th *Theme, txt string) LabelStyle { - return Label(th, th.TextSize*12.0/16.0, txt) -} - -func Overline(th *Theme, txt string) LabelStyle { - return Label(th, th.TextSize*10.0/16.0, txt) -} - -func Label(th *Theme, size unit.Sp, txt string) LabelStyle { - l := LabelStyle{ - Text: txt, - Color: th.Palette.Fg, - SelectionColor: f32color.MulAlpha(th.Palette.ContrastBg, 0x60), - TextSize: size, - Shaper: th.Shaper, - } - l.Font.Typeface = th.Face - return l -} - -func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions { - textColorMacro := op.Record(gtx.Ops) - paint.ColorOp{Color: l.Color}.Add(gtx.Ops) - textColor := textColorMacro.Stop() - selectColorMacro := op.Record(gtx.Ops) - paint.ColorOp{Color: l.SelectionColor}.Add(gtx.Ops) - selectColor := selectColorMacro.Stop() - - if l.State != nil { - if l.State.Text() != l.Text { - l.State.SetText(l.Text) - } - l.State.Alignment = l.Alignment - l.State.MaxLines = l.MaxLines - l.State.Truncator = l.Truncator - l.State.WrapPolicy = l.WrapPolicy - l.State.LineHeight = l.LineHeight - l.State.LineHeightScale = l.LineHeightScale - return l.State.Layout(gtx, l.Shaper, l.Font, l.TextSize, textColor, selectColor) - } - tl := widget.Label{ - Alignment: l.Alignment, - MaxLines: l.MaxLines, - Truncator: l.Truncator, - WrapPolicy: l.WrapPolicy, - LineHeight: l.LineHeight, - LineHeightScale: l.LineHeightScale, - } - return tl.Layout(gtx, l.Shaper, l.Font, l.TextSize, l.Text, textColor) -} diff --git a/gio/widget/material/list.go b/gio/widget/material/list.go deleted file mode 100644 index 01f27e4..0000000 --- a/gio/widget/material/list.go +++ /dev/null @@ -1,323 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image" - "image/color" - "math" - - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -// fromListPosition converts a layout.Position into two floats representing -// the location of the viewport on the underlying content. It needs to know -// the number of elements in the list and the major-axis size of the list -// in order to do this. The returned values will be in the range [0,1], and -// start will be less than or equal to end. -func fromListPosition(lp layout.Position, elements int, majorAxisSize int) (start, end float32) { - // Approximate the size of the scrollable content. - lengthEstPx := float32(lp.Length) - elementLenEstPx := lengthEstPx / float32(elements) - - // Determine how much of the content is visible. - listOffsetF := float32(lp.Offset) - listOffsetL := float32(lp.OffsetLast) - - // Compute the location of the beginning of the viewport using estimated element size and known - // pixel offsets. - viewportStart := clamp1((float32(lp.First)*elementLenEstPx + listOffsetF) / lengthEstPx) - viewportEnd := clamp1((float32(lp.First+lp.Count)*elementLenEstPx + listOffsetL) / lengthEstPx) - viewportFraction := viewportEnd - viewportStart - - // Compute the expected visible proportion of the list content based solely on the ratio - // of the visible size and the estimated total size. - visiblePx := float32(majorAxisSize) - visibleFraction := visiblePx / lengthEstPx - - // Compute the error between the two methods of determining the viewport and diffuse the - // error on either end of the viewport based on how close we are to each end. - err := visibleFraction - viewportFraction - adjStart := viewportStart - adjEnd := viewportEnd - if viewportFraction < 1 { - startShare := viewportStart / (1 - viewportFraction) - endShare := (1 - viewportEnd) / (1 - viewportFraction) - startErr := startShare * err - endErr := endShare * err - - adjStart -= startErr - adjEnd += endErr - } - return adjStart, adjEnd -} - -// rangeIsScrollable returns whether the viewport described by start and end -// is smaller than the underlying content (such that it can be scrolled). -// start and end are expected to each be in the range [0,1], and start -// must be less than or equal to end. -func rangeIsScrollable(start, end float32) bool { - return end-start < 1 -} - -// ScrollTrackStyle configures the presentation of a track for a scroll area. -type ScrollTrackStyle struct { - // MajorPadding and MinorPadding along the major and minor axis of the - // scrollbar's track. This is used to keep the scrollbar from touching - // the edges of the content area. - MajorPadding, MinorPadding unit.Dp - // Color of the track background. - Color color.NRGBA -} - -// ScrollIndicatorStyle configures the presentation of a scroll indicator. -type ScrollIndicatorStyle struct { - // MajorMinLen is the smallest that the scroll indicator is allowed to - // be along the major axis. - MajorMinLen unit.Dp - // MinorWidth is the width of the scroll indicator across the minor axis. - MinorWidth unit.Dp - // Color and HoverColor are the normal and hovered colors of the scroll - // indicator. - Color, HoverColor color.NRGBA - // CornerRadius is the corner radius of the rectangular indicator. 0 - // will produce square corners. 0.5*MinorWidth will produce perfectly - // round corners. - CornerRadius unit.Dp -} - -// ScrollbarStyle configures the presentation of a scrollbar. -type ScrollbarStyle struct { - Scrollbar *widget.Scrollbar - Track ScrollTrackStyle - Indicator ScrollIndicatorStyle -} - -// Scrollbar configures the presentation of a scrollbar using the provided -// theme and state. -func Scrollbar(th *Theme, state *widget.Scrollbar) ScrollbarStyle { - lightFg := th.Palette.Fg - lightFg.A = 150 - darkFg := lightFg - darkFg.A = 200 - - return ScrollbarStyle{ - Scrollbar: state, - Track: ScrollTrackStyle{ - MajorPadding: 2, - MinorPadding: 2, - }, - Indicator: ScrollIndicatorStyle{ - MajorMinLen: th.FingerSize, - MinorWidth: 6, - CornerRadius: 3, - Color: lightFg, - HoverColor: darkFg, - }, - } -} - -// Width returns the minor axis width of the scrollbar in its current -// configuration (taking padding for the scroll track into account). -func (s ScrollbarStyle) Width() unit.Dp { - return s.Indicator.MinorWidth + s.Track.MinorPadding + s.Track.MinorPadding -} - -// Layout the scrollbar. -func (s ScrollbarStyle) Layout(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions { - if !rangeIsScrollable(viewportStart, viewportEnd) { - return layout.Dimensions{} - } - - // Set minimum constraints in an axis-independent way, then convert to - // the correct representation for the current axis. - convert := axis.Convert - maxMajorAxis := convert(gtx.Constraints.Max).X - gtx.Constraints.Min.X = maxMajorAxis - gtx.Constraints.Min.Y = gtx.Dp(s.Width()) - gtx.Constraints.Min = convert(gtx.Constraints.Min) - gtx.Constraints.Max = gtx.Constraints.Min - - s.Scrollbar.Update(gtx, axis, viewportStart, viewportEnd) - - // Darken indicator if hovered. - if s.Scrollbar.IndicatorHovered() { - s.Indicator.Color = s.Indicator.HoverColor - } - - return s.layout(gtx, axis, viewportStart, viewportEnd) -} - -// layout the scroll track and indicator. -func (s ScrollbarStyle) layout(gtx layout.Context, axis layout.Axis, viewportStart, viewportEnd float32) layout.Dimensions { - inset := layout.Inset{ - Top: s.Track.MajorPadding, - Bottom: s.Track.MajorPadding, - Left: s.Track.MinorPadding, - Right: s.Track.MinorPadding, - } - if axis == layout.Horizontal { - inset.Top, inset.Bottom, inset.Left, inset.Right = inset.Left, inset.Right, inset.Top, inset.Bottom - } - - return layout.Background{}.Layout(gtx, - func(gtx layout.Context) layout.Dimensions { - // Lay out the draggable track underneath the scroll indicator. - area := image.Rectangle{ - Max: gtx.Constraints.Min, - } - pointerArea := clip.Rect(area) - defer pointerArea.Push(gtx.Ops).Pop() - s.Scrollbar.AddDrag(gtx.Ops) - - // Stack a normal clickable area on top of the draggable area - // to capture non-dragging clicks. - defer pointer.PassOp{}.Push(gtx.Ops).Pop() - defer pointerArea.Push(gtx.Ops).Pop() - s.Scrollbar.AddTrack(gtx.Ops) - - paint.FillShape(gtx.Ops, s.Track.Color, clip.Rect(area).Op()) - return layout.Dimensions{Size: gtx.Constraints.Min} - }, - func(gtx layout.Context) layout.Dimensions { - return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - // Use axis-independent constraints. - gtx.Constraints.Min = axis.Convert(gtx.Constraints.Min) - gtx.Constraints.Max = axis.Convert(gtx.Constraints.Max) - - // Compute the pixel size and position of the scroll indicator within - // the track. - trackLen := gtx.Constraints.Min.X - viewStart := int(math.Round(float64(viewportStart) * float64(trackLen))) - viewEnd := int(math.Round(float64(viewportEnd) * float64(trackLen))) - indicatorLen := max(viewEnd-viewStart, gtx.Dp(s.Indicator.MajorMinLen)) - if viewStart+indicatorLen > trackLen { - viewStart = trackLen - indicatorLen - } - indicatorDims := axis.Convert(image.Point{ - X: indicatorLen, - Y: gtx.Dp(s.Indicator.MinorWidth), - }) - radius := gtx.Dp(s.Indicator.CornerRadius) - - // Lay out the indicator. - offset := axis.Convert(image.Pt(viewStart, 0)) - defer op.Offset(offset).Push(gtx.Ops).Pop() - paint.FillShape(gtx.Ops, s.Indicator.Color, clip.RRect{ - Rect: image.Rectangle{ - Max: indicatorDims, - }, - SW: radius, - NW: radius, - NE: radius, - SE: radius, - }.Op(gtx.Ops)) - - // Add the indicator pointer hit area. - area := clip.Rect(image.Rectangle{Max: indicatorDims}) - defer pointer.PassOp{}.Push(gtx.Ops).Pop() - defer area.Push(gtx.Ops).Pop() - s.Scrollbar.AddIndicator(gtx.Ops) - - return layout.Dimensions{Size: axis.Convert(gtx.Constraints.Min)} - }) - }, - ) -} - -// AnchorStrategy defines a means of attaching a scrollbar to content. -type AnchorStrategy uint8 - -const ( - // Occupy reserves space for the scrollbar, making the underlying - // content region smaller on one axis. - Occupy AnchorStrategy = iota - // Overlay causes the scrollbar to float atop the content without - // occupying any space. Content in the underlying area can be occluded - // by the scrollbar. - Overlay -) - -// ListStyle configures the presentation of a layout.List with a scrollbar. -type ListStyle struct { - state *widget.List - ScrollbarStyle - AnchorStrategy -} - -// List constructs a ListStyle using the provided theme and state. -func List(th *Theme, state *widget.List) ListStyle { - return ListStyle{ - state: state, - ScrollbarStyle: Scrollbar(th, &state.Scrollbar), - } -} - -// Layout the list and its scrollbar. -func (l ListStyle) Layout(gtx layout.Context, length int, w layout.ListElement) layout.Dimensions { - originalConstraints := gtx.Constraints - - // Determine how much space the scrollbar occupies. - barWidth := gtx.Dp(l.Width()) - - if l.AnchorStrategy == Occupy { - - // Reserve space for the scrollbar using the gtx constraints. - max := l.state.Axis.Convert(gtx.Constraints.Max) - min := l.state.Axis.Convert(gtx.Constraints.Min) - max.Y -= barWidth - if max.Y < 0 { - max.Y = 0 - } - min.Y -= barWidth - if min.Y < 0 { - min.Y = 0 - } - gtx.Constraints.Max = l.state.Axis.Convert(max) - gtx.Constraints.Min = l.state.Axis.Convert(min) - } - - listDims := l.state.List.Layout(gtx, length, w) - gtx.Constraints = originalConstraints - - // Draw the scrollbar. - anchoring := layout.E - if l.state.Axis == layout.Horizontal { - anchoring = layout.S - } - majorAxisSize := l.state.Axis.Convert(listDims.Size).X - start, end := fromListPosition(l.state.Position, length, majorAxisSize) - // layout.Direction respects the minimum, so ensure that the - // scrollbar will be drawn on the correct edge even if the provided - // layout.Context had a zero minimum constraint. - gtx.Constraints.Min = listDims.Size - if l.AnchorStrategy == Occupy { - min := l.state.Axis.Convert(gtx.Constraints.Min) - min.Y += barWidth - gtx.Constraints.Min = l.state.Axis.Convert(min) - } - anchoring.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return l.ScrollbarStyle.Layout(gtx, l.state.Axis, start, end) - }) - - if delta := l.state.ScrollDistance(); delta != 0 { - // Handle any changes to the list position as a result of user interaction - // with the scrollbar. - l.state.List.ScrollBy(delta * float32(length)) - } - - if l.AnchorStrategy == Occupy { - // Increase the width to account for the space occupied by the scrollbar. - cross := l.state.Axis.Convert(listDims.Size) - cross.Y += barWidth - listDims.Size = l.state.Axis.Convert(cross) - } - - return listDims -} diff --git a/gio/widget/material/list_test.go b/gio/widget/material/list_test.go deleted file mode 100644 index daf208f..0000000 --- a/gio/widget/material/list_test.go +++ /dev/null @@ -1,67 +0,0 @@ -package material_test - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" - "github.com/p9c/p9/pkg/gel/gio/widget/material" -) - -func TestListAnchorStrategies(t *testing.T) { - gtx := layout.Context{ - Ops: new(op.Ops), - Metric: unit.Metric{ - PxPerDp: 1, - PxPerSp: 1, - }, - Constraints: layout.Exact(image.Point{ - X: 500, - Y: 500, - }), - } - gtx.Constraints.Min = image.Point{} - - var spaceConstraints layout.Constraints - space := func(gtx layout.Context, index int) layout.Dimensions { - spaceConstraints = gtx.Constraints - if spaceConstraints.Min.X < 0 || spaceConstraints.Min.Y < 0 || - spaceConstraints.Max.X < 0 || spaceConstraints.Max.Y < 0 { - t.Errorf("invalid constraints at index %d: %#+v", index, spaceConstraints) - } - return layout.Dimensions{Size: image.Point{ - X: gtx.Constraints.Max.X, - Y: gtx.Dp(20), - }} - } - - var list widget.List - list.Axis = layout.Vertical - elements := 100 - th := material.NewTheme() - materialList := material.List(th, &list) - indicatorWidth := gtx.Dp(materialList.Width()) - - materialList.AnchorStrategy = material.Occupy - occupyDims := materialList.Layout(gtx, elements, space) - occupyConstraints := spaceConstraints - - materialList.AnchorStrategy = material.Overlay - overlayDims := materialList.Layout(gtx, elements, space) - overlayConstraints := spaceConstraints - - // Both anchor strategies should use all space available if their elements do. - if occupyDims != overlayDims { - t.Errorf("expected occupy dims (%v) to be equal to overlay dims (%v)", occupyDims, overlayDims) - } - // The overlay strategy should not reserve any space for the scroll indicator, - // so the constraints that it presents to its elements should be larger than - // those presented by the occupy strategy. - if overlayConstraints.Max.X != occupyConstraints.Max.X+indicatorWidth { - t.Errorf("overlay max width (%d) != occupy max width (%d) + indicator width (%d)", - overlayConstraints.Max.X, occupyConstraints.Max.X, indicatorWidth) - } -} diff --git a/gio/widget/material/loader.go b/gio/widget/material/loader.go deleted file mode 100644 index eaab22a..0000000 --- a/gio/widget/material/loader.go +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image" - "image/color" - "math" - "time" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -type LoaderStyle struct { - Color color.NRGBA -} - -func Loader(th *Theme) LoaderStyle { - return LoaderStyle{ - Color: th.Palette.ContrastBg, - } -} - -func (l LoaderStyle) Layout(gtx layout.Context) layout.Dimensions { - diam := gtx.Constraints.Min.X - if minY := gtx.Constraints.Min.Y; minY > diam { - diam = minY - } - if diam == 0 { - diam = gtx.Dp(24) - } - sz := gtx.Constraints.Constrain(image.Pt(diam, diam)) - radius := sz.X / 2 - defer op.Offset(image.Pt(radius, radius)).Push(gtx.Ops).Pop() - - dt := float32((time.Duration(gtx.Now.UnixNano()) % (time.Second)).Seconds()) - startAngle := dt * math.Pi * 2 - endAngle := startAngle + math.Pi*1.5 - - defer clipLoader(gtx.Ops, startAngle, endAngle, float32(radius)).Push(gtx.Ops).Pop() - paint.ColorOp{ - Color: l.Color, - }.Add(gtx.Ops) - defer op.Offset(image.Pt(-radius, -radius)).Push(gtx.Ops).Pop() - paint.PaintOp{}.Add(gtx.Ops) - gtx.Execute(op.InvalidateCmd{}) - return layout.Dimensions{ - Size: sz, - } -} - -func clipLoader(ops *op.Ops, startAngle, endAngle, radius float32) clip.Op { - const thickness = .25 - - var ( - width = radius * thickness - delta = endAngle - startAngle - - vy, vx = math.Sincos(float64(startAngle)) - - inner = radius * (1. - thickness*.5) - pen = f32.Pt(float32(vx), float32(vy)).Mul(inner) - center = f32.Pt(0, 0).Sub(pen) - - p clip.Path - ) - - p.Begin(ops) - p.Move(pen) - p.Arc(center, center, delta) - return clip.Stroke{ - Path: p.End(), - Width: width, - }.Op() -} diff --git a/gio/widget/material/progressbar.go b/gio/widget/material/progressbar.go deleted file mode 100644 index 14835a3..0000000 --- a/gio/widget/material/progressbar.go +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image" - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -type ProgressBarStyle struct { - Color color.NRGBA - Height unit.Dp - Radius unit.Dp - TrackColor color.NRGBA - Progress float32 -} - -func ProgressBar(th *Theme, progress float32) ProgressBarStyle { - return ProgressBarStyle{ - Progress: progress, - Height: unit.Dp(4), - Radius: unit.Dp(2), - Color: th.Palette.ContrastBg, - TrackColor: f32color.MulAlpha(th.Palette.Fg, 0x88), - } -} - -func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions { - shader := func(width int, color color.NRGBA) layout.Dimensions { - d := image.Point{X: width, Y: gtx.Dp(p.Height)} - rr := gtx.Dp(p.Radius) - - defer clip.UniformRRect(image.Rectangle{Max: image.Pt(width, d.Y)}, rr).Push(gtx.Ops).Pop() - paint.ColorOp{Color: color}.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - - return layout.Dimensions{Size: d} - } - - progressBarWidth := gtx.Constraints.Max.X - return layout.Stack{Alignment: layout.W}.Layout(gtx, - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - return shader(progressBarWidth, p.TrackColor) - }), - layout.Stacked(func(gtx layout.Context) layout.Dimensions { - fillWidth := int(float32(progressBarWidth) * clamp1(p.Progress)) - fillColor := p.Color - if !gtx.Enabled() { - fillColor = f32color.Disabled(fillColor) - } - if fillWidth < int(p.Radius*2) { - fillWidth = int(p.Radius * 2) - } - return shader(fillWidth, fillColor) - }), - ) -} - -// clamp1 limits v to range [0..1]. -func clamp1(v float32) float32 { - if v >= 1 { - return 1 - } else if v <= 0 { - return 0 - } else { - return v - } -} diff --git a/gio/widget/material/progresscircle.go b/gio/widget/material/progresscircle.go deleted file mode 100644 index 4da99e6..0000000 --- a/gio/widget/material/progresscircle.go +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image" - "image/color" - "math" - - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" -) - -type ProgressCircleStyle struct { - Color color.NRGBA - Progress float32 -} - -func ProgressCircle(th *Theme, progress float32) ProgressCircleStyle { - return ProgressCircleStyle{ - Color: th.Palette.ContrastBg, - Progress: progress, - } -} - -func (p ProgressCircleStyle) Layout(gtx layout.Context) layout.Dimensions { - diam := gtx.Constraints.Min.X - if minY := gtx.Constraints.Min.Y; minY > diam { - diam = minY - } - if diam == 0 { - diam = gtx.Dp(24) - } - sz := gtx.Constraints.Constrain(image.Pt(diam, diam)) - radius := sz.X / 2 - defer op.Offset(image.Pt(radius, radius)).Push(gtx.Ops).Pop() - - defer clipLoader(gtx.Ops, -math.Pi/2, -math.Pi/2+math.Pi*2*p.Progress, float32(radius)).Push(gtx.Ops).Pop() - paint.ColorOp{ - Color: p.Color, - }.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - return layout.Dimensions{ - Size: sz, - } -} diff --git a/gio/widget/material/radiobutton.go b/gio/widget/material/radiobutton.go deleted file mode 100644 index 99aa1e0..0000000 --- a/gio/widget/material/radiobutton.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -type RadioButtonStyle struct { - checkable - Key string - Group *widget.Enum -} - -// RadioButton returns a RadioButton with a label. The key specifies -// the value for the Enum. -func RadioButton(th *Theme, group *widget.Enum, key, label string) RadioButtonStyle { - r := RadioButtonStyle{ - Group: group, - checkable: checkable{ - Label: label, - - Color: th.Palette.Fg, - IconColor: th.Palette.ContrastBg, - TextSize: th.TextSize * 14.0 / 16.0, - Size: 26, - shaper: th.Shaper, - checkedStateIcon: th.Icon.RadioChecked, - uncheckedStateIcon: th.Icon.RadioUnchecked, - }, - Key: key, - } - r.checkable.Font.Typeface = th.Face - return r -} - -// Layout updates enum and displays the radio button. -func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions { - r.Group.Update(gtx) - hovered, hovering := r.Group.Hovered() - focus, focused := r.Group.Focused() - return r.Group.Layout(gtx, r.Key, func(gtx layout.Context) layout.Dimensions { - semantic.RadioButton.Add(gtx.Ops) - highlight := hovering && hovered == r.Key || focused && focus == r.Key - return r.layout(gtx, r.Group.Value == r.Key, highlight) - }) -} diff --git a/gio/widget/material/slider.go b/gio/widget/material/slider.go deleted file mode 100644 index fbbde52..0000000 --- a/gio/widget/material/slider.go +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image" - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -// Slider is for selecting a value in a range. -func Slider(th *Theme, float *widget.Float) SliderStyle { - return SliderStyle{ - Color: th.Palette.ContrastBg, - Float: float, - FingerSize: th.FingerSize, - } -} - -type SliderStyle struct { - Axis layout.Axis - Color color.NRGBA - Float *widget.Float - - FingerSize unit.Dp -} - -func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions { - const thumbRadius unit.Dp = 6 - tr := gtx.Dp(thumbRadius) - trackWidth := gtx.Dp(2) - - axis := s.Axis - // Keep a minimum length so that the track is always visible. - minLength := tr + 3*tr + tr - // Try to expand to finger size, but only if the constraints - // allow for it. - touchSizePx := min(gtx.Dp(s.FingerSize), axis.Convert(gtx.Constraints.Max).Y) - sizeMain := max(axis.Convert(gtx.Constraints.Min).X, minLength) - sizeCross := max(2*tr, touchSizePx) - size := axis.Convert(image.Pt(sizeMain, sizeCross)) - - o := axis.Convert(image.Pt(tr, 0)) - trans := op.Offset(o).Push(gtx.Ops) - gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*tr, sizeCross)) - dims := s.Float.Layout(gtx, axis, thumbRadius) - gtx.Constraints.Min = gtx.Constraints.Min.Add(axis.Convert(image.Pt(0, sizeCross))) - thumbPos := tr + int(s.Float.Value*float32(axis.Convert(dims.Size).X)) - trans.Pop() - - color := s.Color - if !gtx.Enabled() { - color = f32color.Disabled(color) - } - - rect := func(minx, miny, maxx, maxy int) image.Rectangle { - r := image.Rect(minx, miny, maxx, maxy) - if axis == layout.Vertical { - r.Max.X, r.Min.X = sizeMain-r.Min.X, sizeMain-r.Max.X - } - r.Min = axis.Convert(r.Min) - r.Max = axis.Convert(r.Max) - return r - } - - // Draw track before thumb. - track := rect( - tr, sizeCross/2-trackWidth/2, - thumbPos, sizeCross/2+trackWidth/2, - ) - paint.FillShape(gtx.Ops, color, clip.Rect(track).Op()) - - // Draw track after thumb. - track = rect( - thumbPos, axis.Convert(track.Min).Y, - sizeMain-tr, axis.Convert(track.Max).Y, - ) - paint.FillShape(gtx.Ops, f32color.MulAlpha(color, 96), clip.Rect(track).Op()) - - // Draw thumb. - pt := image.Pt(thumbPos, sizeCross/2) - thumb := rect( - pt.X-tr, pt.Y-tr, - pt.X+tr, pt.Y+tr, - ) - paint.FillShape(gtx.Ops, color, clip.Ellipse(thumb).Op(gtx.Ops)) - - return layout.Dimensions{Size: size} -} diff --git a/gio/widget/material/switch.go b/gio/widget/material/switch.go deleted file mode 100644 index 1965d86..0000000 --- a/gio/widget/material/switch.go +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image" - "image/color" - - "github.com/p9c/p9/pkg/gel/gio/internal/f32color" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -type SwitchStyle struct { - Description string - Color struct { - Enabled color.NRGBA - Disabled color.NRGBA - Track color.NRGBA - } - Switch *widget.Bool -} - -// Switch is for selecting a boolean value. -func Switch(th *Theme, swtch *widget.Bool, description string) SwitchStyle { - sw := SwitchStyle{ - Switch: swtch, - Description: description, - } - sw.Color.Enabled = th.Palette.ContrastBg - sw.Color.Disabled = th.Palette.Bg - sw.Color.Track = f32color.MulAlpha(th.Palette.Fg, 0x88) - return sw -} - -// Layout updates the switch and displays it. -func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions { - s.Switch.Update(gtx) - trackWidth := gtx.Dp(36) - trackHeight := gtx.Dp(16) - thumbSize := gtx.Dp(20) - trackOff := (thumbSize - trackHeight) / 2 - - // Draw track. - trackCorner := trackHeight / 2 - trackRect := image.Rectangle{Max: image.Point{ - X: trackWidth, - Y: trackHeight, - }} - col := s.Color.Disabled - if s.Switch.Value { - col = s.Color.Enabled - } - if !gtx.Enabled() { - col = f32color.Disabled(col) - } - trackColor := s.Color.Track - t := op.Offset(image.Point{Y: trackOff}).Push(gtx.Ops) - cl := clip.UniformRRect(trackRect, trackCorner).Push(gtx.Ops) - paint.ColorOp{Color: trackColor}.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - cl.Pop() - t.Pop() - - // Draw thumb ink. - inkSize := gtx.Dp(44) - rr := inkSize / 2 - inkOff := image.Point{ - X: trackWidth/2 - rr, - Y: -rr + trackHeight/2 + trackOff, - } - t = op.Offset(inkOff).Push(gtx.Ops) - gtx.Constraints.Min = image.Pt(inkSize, inkSize) - cl = clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops) - for _, p := range s.Switch.History() { - drawInk(gtx, p) - } - cl.Pop() - t.Pop() - - // Compute thumb offset. - if s.Switch.Value { - xoff := trackWidth - thumbSize - defer op.Offset(image.Point{X: xoff}).Push(gtx.Ops).Pop() - } - - thumbRadius := thumbSize / 2 - - circle := func(x, y, r int) clip.Op { - b := image.Rectangle{ - Min: image.Pt(x-r, y-r), - Max: image.Pt(x+r, y+r), - } - return clip.Ellipse(b).Op(gtx.Ops) - } - // Draw hover. - if s.Switch.Hovered() || gtx.Focused(s.Switch) { - r := thumbRadius * 10 / 17 - background := f32color.MulAlpha(s.Color.Enabled, 70) - paint.FillShape(gtx.Ops, background, circle(thumbRadius, thumbRadius, r)) - } - - // Draw thumb shadow, a translucent disc slightly larger than the - // thumb itself. - // Center shadow horizontally and slightly adjust its Y. - paint.FillShape(gtx.Ops, argb(0x55000000), circle(thumbRadius, thumbRadius+gtx.Dp(.25), thumbRadius+1)) - - // Draw thumb. - paint.FillShape(gtx.Ops, col, circle(thumbRadius, thumbRadius, thumbRadius)) - - // Set up click area. - clickSize := gtx.Dp(40) - clickOff := image.Point{ - X: (thumbSize - clickSize) / 2, - Y: (trackHeight-clickSize)/2 + trackOff, - } - defer op.Offset(clickOff).Push(gtx.Ops).Pop() - sz := image.Pt(clickSize, clickSize) - defer clip.Ellipse(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop() - s.Switch.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - if d := s.Description; d != "" { - semantic.DescriptionOp(d).Add(gtx.Ops) - } - semantic.Switch.Add(gtx.Ops) - return layout.Dimensions{Size: sz} - }) - - dims := image.Point{X: trackWidth, Y: thumbSize} - return layout.Dimensions{Size: dims} -} diff --git a/gio/widget/material/theme.go b/gio/widget/material/theme.go deleted file mode 100644 index c9740ad..0000000 --- a/gio/widget/material/theme.go +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package material - -import ( - "image/color" - - "golang.org/x/exp/shiny/materialdesign/icons" - - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -// Palette contains the minimal set of colors that a widget may need to -// draw itself. -type Palette struct { - // Bg is the background color atop which content is currently being - // drawn. - Bg color.NRGBA - - // Fg is a color suitable for drawing on top of Bg. - Fg color.NRGBA - - // ContrastBg is a color used to draw attention to active, - // important, interactive widgets such as buttons. - ContrastBg color.NRGBA - - // ContrastFg is a color suitable for content drawn on top of - // ContrastBg. - ContrastFg color.NRGBA -} - -// Theme holds the general theme of an app or window. Different top-level -// windows should have different instances of Theme (with different Shapers; -// see the godoc for [text.Shaper]), though their other fields can be equal. -type Theme struct { - Shaper *text.Shaper - Palette - TextSize unit.Sp - Icon struct { - CheckBoxChecked *widget.Icon - CheckBoxUnchecked *widget.Icon - RadioChecked *widget.Icon - RadioUnchecked *widget.Icon - } - // Face selects the default typeface for text. - Face font.Typeface - - // FingerSize is the minimum touch target size. - FingerSize unit.Dp -} - -// NewTheme constructs a theme (and underlying text shaper). -func NewTheme() *Theme { - t := &Theme{Shaper: &text.Shaper{}} - t.Palette = Palette{ - Fg: rgb(0x000000), - Bg: rgb(0xffffff), - ContrastBg: rgb(0x3f51b5), - ContrastFg: rgb(0xffffff), - } - t.TextSize = 16 - - t.Icon.CheckBoxChecked = mustIcon(widget.NewIcon(icons.ToggleCheckBox)) - t.Icon.CheckBoxUnchecked = mustIcon(widget.NewIcon(icons.ToggleCheckBoxOutlineBlank)) - t.Icon.RadioChecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonChecked)) - t.Icon.RadioUnchecked = mustIcon(widget.NewIcon(icons.ToggleRadioButtonUnchecked)) - - // 38dp is on the lower end of possible finger size. - t.FingerSize = 38 - - return t -} - -func (t Theme) WithPalette(p Palette) Theme { - t.Palette = p - return t -} - -func mustIcon(ic *widget.Icon, err error) *widget.Icon { - if err != nil { - panic(err) - } - return ic -} - -func rgb(c uint32) color.NRGBA { - return argb(0xff000000 | c) -} - -func argb(c uint32) color.NRGBA { - return color.NRGBA{A: uint8(c >> 24), R: uint8(c >> 16), G: uint8(c >> 8), B: uint8(c)} -} diff --git a/gio/widget/selectable.go b/gio/widget/selectable.go deleted file mode 100644 index 68610a2..0000000 --- a/gio/widget/selectable.go +++ /dev/null @@ -1,391 +0,0 @@ -package widget - -import ( - "image" - "io" - "math" - "strings" - - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/clipboard" - "github.com/p9c/p9/pkg/gel/gio/io/event" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -// stringSource is an immutable textSource with a fixed string -// value. -type stringSource struct { - reader *strings.Reader -} - -var _ textSource = stringSource{} - -func newStringSource(str string) stringSource { - return stringSource{ - reader: strings.NewReader(str), - } -} - -func (s stringSource) Changed() bool { - return false -} - -func (s stringSource) Size() int64 { - return s.reader.Size() -} - -func (s stringSource) ReadAt(b []byte, offset int64) (int, error) { - return s.reader.ReadAt(b, offset) -} - -// ReplaceRunes is unimplemented, as a stringSource is immutable. -func (s stringSource) ReplaceRunes(byteOffset, runeCount int64, str string) { -} - -// Selectable displays selectable text. -type Selectable struct { - // Alignment controls the alignment of the text. - Alignment text.Alignment - // MaxLines is the maximum number of lines of text to be displayed. - MaxLines int - // Truncator is the symbol to use at the end of the final line of text - // if text was cut off. Defaults to "…" if left empty. - Truncator string - // WrapPolicy configures how displayed text will be broken into lines. - WrapPolicy text.WrapPolicy - // LineHeight controls the distance between the baselines of lines of text. - // If zero, a sensible default will be used. - LineHeight unit.Sp - // LineHeightScale applies a scaling factor to the LineHeight. If zero, a - // sensible default will be used. - LineHeightScale float32 - initialized bool - source stringSource - // scratch is a buffer reused to efficiently read text out of the - // textView. - scratch []byte - lastValue string - text textView - focused bool - dragging bool - dragger gesture.Drag - - clicker gesture.Click -} - -// initialize must be called at the beginning of any exported method that -// manipulates text state. It ensures that the underlying text is safe to -// access. -func (l *Selectable) initialize() { - if !l.initialized { - l.source = newStringSource("") - l.text.SetSource(l.source) - l.initialized = true - } -} - -// Focused returns whether the label is focused or not. -func (l *Selectable) Focused() bool { - return l.focused -} - -// paintSelection paints the contrasting background for selected text. -func (l *Selectable) paintSelection(gtx layout.Context, material op.CallOp) { - l.initialize() - if !l.focused { - return - } - l.text.PaintSelection(gtx, material) -} - -// paintText paints the text glyphs with the provided material. -func (l *Selectable) paintText(gtx layout.Context, material op.CallOp) { - l.initialize() - l.text.PaintText(gtx, material) -} - -// SelectionLen returns the length of the selection, in runes; it is -// equivalent to utf8.RuneCountInString(e.SelectedText()). -func (l *Selectable) SelectionLen() int { - l.initialize() - return l.text.SelectionLen() -} - -// Selection returns the start and end of the selection, as rune offsets. -// start can be > end. -func (l *Selectable) Selection() (start, end int) { - l.initialize() - return l.text.Selection() -} - -// SetCaret moves the caret to start, and sets the selection end to end. start -// and end are in runes, and represent offsets into the editor text. -func (l *Selectable) SetCaret(start, end int) { - l.initialize() - l.text.SetCaret(start, end) -} - -// SelectedText returns the currently selected text (if any) from the editor. -func (l *Selectable) SelectedText() string { - l.initialize() - l.scratch = l.text.SelectedText(l.scratch) - return string(l.scratch) -} - -// ClearSelection clears the selection, by setting the selection end equal to -// the selection start. -func (l *Selectable) ClearSelection() { - l.initialize() - l.text.ClearSelection() -} - -// Text returns the contents of the label. -func (l *Selectable) Text() string { - l.initialize() - l.scratch = l.text.Text(l.scratch) - return string(l.scratch) -} - -// SetText updates the text to s if it does not already contain s. Updating the -// text will clear the selection unless the selectable already contains s. -func (l *Selectable) SetText(s string) { - l.initialize() - if l.lastValue != s { - l.source = newStringSource(s) - l.lastValue = s - l.text.SetSource(l.source) - } -} - -// Truncated returns whether the text has been truncated by the text shaper to -// fit within available constraints. -func (l *Selectable) Truncated() bool { - return l.text.Truncated() -} - -// Update the state of the selectable in response to input events. It returns whether the -// text selection changed during event processing. -func (l *Selectable) Update(gtx layout.Context) bool { - l.initialize() - return l.handleEvents(gtx) -} - -// Layout clips to the dimensions of the selectable, updates the shaped text, configures input handling, and paints -// the text and selection rectangles. The provided textMaterial and selectionMaterial ops are used to set the -// paint material for the text and selection rectangles, respectively. -func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectionMaterial op.CallOp) layout.Dimensions { - l.Update(gtx) - l.text.LineHeight = l.LineHeight - l.text.LineHeightScale = l.LineHeightScale - l.text.Alignment = l.Alignment - l.text.MaxLines = l.MaxLines - l.text.Truncator = l.Truncator - l.text.WrapPolicy = l.WrapPolicy - l.text.Layout(gtx, lt, font, size) - dims := l.text.Dimensions() - defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop() - pointer.CursorText.Add(gtx.Ops) - event.Op(gtx.Ops, l) - - l.clicker.Add(gtx.Ops) - l.dragger.Add(gtx.Ops) - - l.paintSelection(gtx, selectionMaterial) - l.paintText(gtx, textMaterial) - return dims -} - -func (l *Selectable) handleEvents(gtx layout.Context) (selectionChanged bool) { - oldStart, oldLen := min(l.text.Selection()), l.text.SelectionLen() - defer func() { - if newStart, newLen := min(l.text.Selection()), l.text.SelectionLen(); oldStart != newStart || oldLen != newLen { - selectionChanged = true - } - }() - l.processPointer(gtx) - l.processKey(gtx) - return selectionChanged -} - -func (e *Selectable) processPointer(gtx layout.Context) { - for _, evt := range e.clickDragEvents(gtx) { - switch evt := evt.(type) { - case gesture.ClickEvent: - switch { - case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse, - evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse: - prevCaretPos, _ := e.text.Selection() - e.text.MoveCoord(image.Point{ - X: int(math.Round(float64(evt.Position.X))), - Y: int(math.Round(float64(evt.Position.Y))), - }) - gtx.Execute(key.FocusCmd{Tag: e}) - if evt.Modifiers == key.ModShift { - start, end := e.text.Selection() - // If they clicked closer to the end, then change the end to - // where the caret used to be (effectively swapping start & end). - if abs(end-start) < abs(start-prevCaretPos) { - e.text.SetCaret(start, prevCaretPos) - } - } else { - e.text.ClearSelection() - } - e.dragging = true - - // Process multi-clicks. - switch { - case evt.NumClicks == 2: - e.text.MoveWord(-1, selectionClear) - e.text.MoveWord(1, selectionExtend) - e.dragging = false - case evt.NumClicks >= 3: - e.text.MoveLineStart(selectionClear) - e.text.MoveLineEnd(selectionExtend) - e.dragging = false - } - } - case pointer.Event: - release := false - switch { - case evt.Kind == pointer.Release && evt.Source == pointer.Mouse: - release = true - fallthrough - case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse: - if e.dragging { - e.text.MoveCoord(image.Point{ - X: int(math.Round(float64(evt.Position.X))), - Y: int(math.Round(float64(evt.Position.Y))), - }) - - if release { - e.dragging = false - } - } - } - } - } -} - -func (e *Selectable) clickDragEvents(gtx layout.Context) []event.Event { - var combinedEvents []event.Event - for { - evt, ok := e.clicker.Update(gtx.Source) - if !ok { - break - } - combinedEvents = append(combinedEvents, evt) - } - for { - evt, ok := e.dragger.Update(gtx.Metric, gtx.Source, gesture.Both) - if !ok { - break - } - combinedEvents = append(combinedEvents, evt) - } - return combinedEvents -} - -func (e *Selectable) processKey(gtx layout.Context) { - for { - ke, ok := gtx.Event( - key.FocusFilter{Target: e}, - key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Focus: e, Name: key.NameRightArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Focus: e, Name: key.NameUpArrow, Optional: key.ModShortcutAlt | key.ModShift}, - key.Filter{Focus: e, Name: key.NameDownArrow, Optional: key.ModShortcutAlt | key.ModShift}, - - key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift}, - key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift}, - key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift}, - key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift}, - - key.Filter{Focus: e, Name: "C", Required: key.ModShortcut}, - key.Filter{Focus: e, Name: "X", Required: key.ModShortcut}, - key.Filter{Focus: e, Name: "A", Required: key.ModShortcut}, - ) - if !ok { - break - } - switch ke := ke.(type) { - case key.FocusEvent: - e.focused = ke.Focus - case key.Event: - if !e.focused || ke.State != key.Press { - break - } - e.command(gtx, ke) - } - } -} - -func (e *Selectable) command(gtx layout.Context, k key.Event) { - direction := 1 - if gtx.Locale.Direction.Progression() == system.TowardOrigin { - direction = -1 - } - moveByWord := k.Modifiers.Contain(key.ModShortcutAlt) - selAct := selectionClear - if k.Modifiers.Contain(key.ModShift) { - selAct = selectionExtend - } - if k.Modifiers == key.ModShortcut { - switch k.Name { - // Copy or Cut selection -- ignored if nothing selected. - case "C", "X": - e.scratch = e.text.SelectedText(e.scratch) - if text := string(e.scratch); text != "" { - gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(strings.NewReader(text))}) - } - // Select all - case "A": - e.text.SetCaret(0, e.text.Len()) - } - return - } - switch k.Name { - case key.NameUpArrow: - e.text.MoveLines(-1, selAct) - case key.NameDownArrow: - e.text.MoveLines(+1, selAct) - case key.NameLeftArrow: - if moveByWord { - e.text.MoveWord(-1*direction, selAct) - } else { - if selAct == selectionClear { - e.text.ClearSelection() - } - e.text.MoveCaret(-1*direction, -1*direction*int(selAct)) - } - case key.NameRightArrow: - if moveByWord { - e.text.MoveWord(1*direction, selAct) - } else { - if selAct == selectionClear { - e.text.ClearSelection() - } - e.text.MoveCaret(1*direction, int(selAct)*direction) - } - case key.NamePageUp: - e.text.MovePages(-1, selAct) - case key.NamePageDown: - e.text.MovePages(+1, selAct) - case key.NameHome: - e.text.MoveLineStart(selAct) - case key.NameEnd: - e.text.MoveLineEnd(selAct) - } -} - -// Regions returns visible regions covering the rune range [start,end). -func (l *Selectable) Regions(start, end int, regions []Region) []Region { - l.initialize() - return l.text.Regions(start, end, regions) -} diff --git a/gio/widget/selectable_test.go b/gio/widget/selectable_test.go deleted file mode 100644 index 17cccde..0000000 --- a/gio/widget/selectable_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package widget - -import ( - "fmt" - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/font/gofont" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/key" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" -) - -func TestSelectableZeroValue(t *testing.T) { - var s Selectable - if s.Text() != "" { - t.Errorf("expected zero value to have no text, got %q", s.Text()) - } - if start, end := s.Selection(); start != 0 || end != 0 { - t.Errorf("expected start=0, end=0, got start=%d, end=%d", start, end) - } - if selected := s.SelectedText(); selected != "" { - t.Errorf("expected selected text to be \"\", got %q", selected) - } - s.SetCaret(5, 5) - if start, end := s.Selection(); start != 0 || end != 0 { - t.Errorf("expected start=0, end=0, got start=%d, end=%d", start, end) - } -} - -// Verify that an existing selection is dismissed when you press arrow keys. -func TestSelectableMove(t *testing.T) { - r := new(input.Router) - gtx := layout.Context{ - Ops: new(op.Ops), - Locale: english, - Source: r.Source(), - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fnt := font.Font{} - fontSize := unit.Sp(10) - - str := `0123456789` - - // Layout once to populate e.lines and get focus. - s := new(Selectable) - - gtx.Execute(key.FocusCmd{Tag: s}) - s.SetText(str) - // Set up selection so the Selectable filters for all 4 directional keys. - s.Layout(gtx, cache, font.Font{}, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - s.SetCaret(3, 6) - s.Layout(gtx, cache, font.Font{}, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - s.Layout(gtx, cache, font.Font{}, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - - for _, keyName := range []key.Name{key.NameLeftArrow, key.NameRightArrow, key.NameUpArrow, key.NameDownArrow} { - // Select 345 - s.SetCaret(3, 6) - if start, end := s.Selection(); start != 3 || end != 6 { - t.Errorf("expected start=%d, end=%d, got start=%d, end=%d", 3, 6, start, end) - } - if expected, got := "345", s.SelectedText(); expected != got { - t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) - } - - // Press the key - r.Queue(key.Event{State: key.Press, Name: keyName}) - s.SetText(str) - s.Layout(gtx, cache, fnt, fontSize, op.CallOp{}, op.CallOp{}) - r.Frame(gtx.Ops) - - if expected, got := "", s.SelectedText(); expected != got { - t.Errorf("KeyName %s, expected %q, got %q", keyName, expected, got) - } - } -} - -func TestSelectableConfigurations(t *testing.T) { - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Exact(image.Pt(300, 300)), - Locale: english, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection())) - fontSize := unit.Sp(10) - font := font.Font{} - sentence := "\n\n\n\n\n\n\n\n\n\n\n\nthe quick brown fox jumps over the lazy dog" - - for _, alignment := range []text.Alignment{text.Start, text.Middle, text.End} { - for _, zeroMin := range []bool{true, false} { - t.Run(fmt.Sprintf("Alignment: %v ZeroMinConstraint: %v", alignment, zeroMin), func(t *testing.T) { - defer func() { - if err := recover(); err != nil { - t.Error(err) - } - }() - if zeroMin { - gtx.Constraints.Min = image.Point{} - } else { - gtx.Constraints.Min = gtx.Constraints.Max - } - s := new(Selectable) - s.Alignment = alignment - s.SetText(sentence) - interactiveDims := s.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - staticDims := Label{Alignment: alignment}.Layout(gtx, cache, font, fontSize, sentence, op.CallOp{}) - - if interactiveDims != staticDims { - t.Errorf("expected consistent dimensions, static returned %#+v, interactive returned %#+v", staticDims, interactiveDims) - } - }) - } - } -} diff --git a/gio/widget/text.go b/gio/widget/text.go deleted file mode 100644 index 7c1dc81..0000000 --- a/gio/widget/text.go +++ /dev/null @@ -1,852 +0,0 @@ -package widget - -import ( - "bufio" - "image" - "io" - "math" - "slices" - "sort" - "unicode" - "unicode/utf8" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "golang.org/x/image/math/fixed" -) - -// textSource provides text data for use in widgets. If the underlying data type -// can fail due to I/O errors, it is the responsibility of that type to provide -// its own mechanism to surface and handle those errors. They will not always -// be returned by widgets using these functions. -type textSource interface { - io.ReaderAt - // Size returns the total length of the data in bytes. - Size() int64 - // Changed returns whether the contents have changed since the last call - // to Changed. - Changed() bool - // ReplaceRunes replaces runeCount runes starting at byteOffset within the - // data with the provided string. Implementations of read-only text sources - // are free to make this a no-op. - ReplaceRunes(byteOffset int64, runeCount int64, replacement string) -} - -// textView provides efficient shaping and indexing of interactive text. When provided -// with a TextSource, textView will shape and cache the runes within that source. -// It provides methods for configuring a viewport onto the shaped text which can -// be scrolled, and for configuring and drawing text selection boxes. -type textView struct { - Alignment text.Alignment - // LineHeight controls the distance between the baselines of lines of text. - // If zero, a sensible default will be used. - LineHeight unit.Sp - // LineHeightScale applies a scaling factor to the LineHeight. If zero, a - // sensible default will be used. - LineHeightScale float32 - // SingleLine forces the text to stay on a single line. - // SingleLine also sets the scrolling direction to - // horizontal. - SingleLine bool - // MaxLines limits the shaped text to a specific quantity of shaped lines. - MaxLines int - // Truncator is the text that will be shown at the end of the final - // line if MaxLines is exceeded. Defaults to "…" if empty. - Truncator string - // WrapPolicy configures how displayed text will be broken into lines. - WrapPolicy text.WrapPolicy - // DisableSpaceTrim configures whether trailing whitespace on a line will have its - // width zeroed. Set to true for editors, but false for non-editable text. - DisableSpaceTrim bool - // Mask replaces the visual display of each rune in the contents with the given rune. - // Newline characters are not masked. When non-zero, the unmasked contents - // are accessed by Len, Text, and SetText. - Mask rune - - params text.Parameters - shaper *text.Shaper - seekCursor int64 - rr textSource - maskReader maskReader - // graphemes tracks the indices of grapheme cluster boundaries within rr. - graphemes []int - // paragraphReader is used to populate graphemes. - paragraphReader graphemeReader - lastMask rune - viewSize image.Point - valid bool - regions []Region - dims layout.Dimensions - - // offIndex is an index of rune index to byte offsets. - offIndex []offEntry - - index glyphIndex - - caret struct { - // xoff is the offset to the current position when moving between lines. - xoff fixed.Int26_6 - // start is the current caret position in runes, and also the start position of - // selected text. end is the end position of selected text. If start - // == end, then there's no selection. Note that it's possible (and - // common) that the caret (start) is after the end, e.g. after - // Shift-DownArrow. - start int - end int - } - - scrollOff image.Point -} - -func (e *textView) Changed() bool { - return e.rr.Changed() -} - -// Dimensions returns the dimensions of the visible text. -func (e *textView) Dimensions() layout.Dimensions { - basePos := e.dims.Size.Y - e.dims.Baseline - return layout.Dimensions{Size: e.viewSize, Baseline: e.viewSize.Y - basePos} -} - -// FullDimensions returns the dimensions of all shaped text, including -// text that isn't visible within the current viewport. -func (e *textView) FullDimensions() layout.Dimensions { - return e.dims -} - -// SetSource initializes the underlying data source for the Text. This -// must be done before invoking any other methods on Text. -func (e *textView) SetSource(source textSource) { - e.rr = source - e.invalidate() - e.seekCursor = 0 -} - -// ReadRuneAt reads the rune starting at the given byte offset, if any. -func (e *textView) ReadRuneAt(off int64) (rune, int, error) { - var buf [utf8.UTFMax]byte - b := buf[:] - n, err := e.rr.ReadAt(b, off) - b = b[:n] - r, s := utf8.DecodeRune(b) - return r, s, err -} - -// ReadRuneAt reads the run prior to the given byte offset, if any. -func (e *textView) ReadRuneBefore(off int64) (rune, int, error) { - var buf [utf8.UTFMax]byte - b := buf[:] - if off < utf8.UTFMax { - b = b[:off] - off = 0 - } else { - off -= utf8.UTFMax - } - n, err := e.rr.ReadAt(b, off) - b = b[:n] - r, s := utf8.DecodeLastRune(b) - return r, s, err -} - -func (e *textView) makeValid() { - if e.valid { - return - } - e.layoutText(e.shaper) - e.valid = true -} - -func (e *textView) closestToRune(runeIdx int) combinedPos { - e.makeValid() - pos, _ := e.index.closestToRune(runeIdx) - return pos -} - -func (e *textView) closestToLineCol(line, col int) combinedPos { - e.makeValid() - return e.index.closestToLineCol(screenPos{line: line, col: col}) -} - -func (e *textView) closestToXY(x fixed.Int26_6, y int) combinedPos { - e.makeValid() - return e.index.closestToXY(x, y) -} - -func (e *textView) closestToXYGraphemes(x fixed.Int26_6, y int) combinedPos { - // Find the closest existing rune position to the provided coordinates. - pos := e.closestToXY(x, y) - // Resolve cluster boundaries on either side of the rune position. - firstOption := e.moveByGraphemes(pos.runes, 0) - distance := 1 - if firstOption > pos.runes { - distance = -1 - } - secondOption := e.moveByGraphemes(firstOption, distance) - // Choose the closest grapheme cluster boundary to the desired point. - first := e.closestToRune(firstOption) - firstDist := absFixed(first.x - x) - second := e.closestToRune(secondOption) - secondDist := absFixed(second.x - x) - if firstDist > secondDist { - return second - } else { - return first - } -} - -func absFixed(i fixed.Int26_6) fixed.Int26_6 { - if i < 0 { - return -i - } - return i -} - -// MaxLines moves the cursor the specified number of lines vertically, ensuring -// that the resulting position is aligned to a grapheme cluster. -func (e *textView) MoveLines(distance int, selAct selectionAction) { - caretStart := e.closestToRune(e.caret.start) - x := caretStart.x + e.caret.xoff - // Seek to line. - pos := e.closestToLineCol(caretStart.lineCol.line+distance, 0) - pos = e.closestToXYGraphemes(x, pos.y) - e.caret.start = pos.runes - e.caret.xoff = x - pos.x - e.updateSelection(selAct) -} - -// calculateViewSize determines the size of the current visible content, -// ensuring that even if there is no text content, some space is reserved -// for the caret. -func (e *textView) calculateViewSize(gtx layout.Context) image.Point { - base := e.dims.Size - if caretWidth := e.caretWidth(gtx); base.X < caretWidth { - base.X = caretWidth - } - return gtx.Constraints.Constrain(base) -} - -// Layout the text, reshaping it as necessary. -func (e *textView) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp) { - if e.params.Locale != gtx.Locale { - e.params.Locale = gtx.Locale - e.invalidate() - } - textSize := fixed.I(gtx.Sp(size)) - if e.params.Font != font || e.params.PxPerEm != textSize { - e.invalidate() - e.params.Font = font - e.params.PxPerEm = textSize - } - maxWidth := gtx.Constraints.Max.X - if e.SingleLine { - maxWidth = math.MaxInt - } - minWidth := gtx.Constraints.Min.X - if maxWidth != e.params.MaxWidth { - e.params.MaxWidth = maxWidth - e.invalidate() - } - if minWidth != e.params.MinWidth { - e.params.MinWidth = minWidth - e.invalidate() - } - if lt != e.shaper { - e.shaper = lt - e.invalidate() - } - if e.Mask != e.lastMask { - e.lastMask = e.Mask - e.invalidate() - } - if e.Alignment != e.params.Alignment { - e.params.Alignment = e.Alignment - e.invalidate() - } - if e.Truncator != e.params.Truncator { - e.params.Truncator = e.Truncator - e.invalidate() - } - if e.MaxLines != e.params.MaxLines { - e.params.MaxLines = e.MaxLines - e.invalidate() - } - if e.WrapPolicy != e.params.WrapPolicy { - e.params.WrapPolicy = e.WrapPolicy - e.invalidate() - } - if lh := fixed.I(gtx.Sp(e.LineHeight)); lh != e.params.LineHeight { - e.params.LineHeight = lh - e.invalidate() - } - if e.LineHeightScale != e.params.LineHeightScale { - e.params.LineHeightScale = e.LineHeightScale - e.invalidate() - } - if e.DisableSpaceTrim != e.params.DisableSpaceTrim { - e.params.DisableSpaceTrim = e.DisableSpaceTrim - e.invalidate() - } - - e.makeValid() - - if viewSize := e.calculateViewSize(gtx); viewSize != e.viewSize { - e.viewSize = viewSize - e.invalidate() - } - e.makeValid() -} - -// PaintSelection clips and paints the visible text selection rectangles using -// the provided material to fill the rectangles. -func (e *textView) PaintSelection(gtx layout.Context, material op.CallOp) { - localViewport := image.Rectangle{Max: e.viewSize} - docViewport := image.Rectangle{Max: e.viewSize}.Add(e.scrollOff) - defer clip.Rect(localViewport).Push(gtx.Ops).Pop() - e.regions = e.index.locate(docViewport, e.caret.start, e.caret.end, e.regions) - for _, region := range e.regions { - area := clip.Rect(region.Bounds).Push(gtx.Ops) - material.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - area.Pop() - } -} - -// PaintText clips and paints the visible text glyph outlines using the provided -// material to fill the glyphs. -func (e *textView) PaintText(gtx layout.Context, material op.CallOp) { - m := op.Record(gtx.Ops) - viewport := image.Rectangle{ - Min: e.scrollOff, - Max: e.viewSize.Add(e.scrollOff), - } - it := textIterator{ - viewport: viewport, - material: material, - } - - startGlyph := 0 - for _, line := range e.index.lines { - if line.descent.Ceil()+line.yOff >= viewport.Min.Y { - break - } - startGlyph += line.glyphs - } - var glyphs [32]text.Glyph - line := glyphs[:0] - for _, g := range e.index.glyphs[startGlyph:] { - var ok bool - if line, ok = it.paintGlyph(gtx, e.shaper, g, line); !ok { - break - } - } - - call := m.Stop() - viewport.Min = viewport.Min.Add(it.padding.Min) - viewport.Max = viewport.Max.Add(it.padding.Max) - defer clip.Rect(viewport.Sub(e.scrollOff)).Push(gtx.Ops).Pop() - call.Add(gtx.Ops) -} - -// caretWidth returns the width occupied by the caret for the current -// gtx. -func (e *textView) caretWidth(gtx layout.Context) int { - carWidth2 := gtx.Dp(1) / 2 - if carWidth2 < 1 { - carWidth2 = 1 - } - return carWidth2 -} - -// PaintCaret clips and paints the caret rectangle, adding material immediately -// before painting to set the appropriate paint material. -func (e *textView) PaintCaret(gtx layout.Context, material op.CallOp) { - carWidth2 := e.caretWidth(gtx) - caretPos, carAsc, carDesc := e.CaretInfo() - - carRect := image.Rectangle{ - Min: caretPos.Sub(image.Pt(carWidth2, carAsc)), - Max: caretPos.Add(image.Pt(carWidth2, carDesc)), - } - cl := image.Rectangle{Max: e.viewSize} - carRect = cl.Intersect(carRect) - if !carRect.Empty() { - defer clip.Rect(carRect).Push(gtx.Ops).Pop() - material.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - } -} - -func (e *textView) CaretInfo() (pos image.Point, ascent, descent int) { - caretStart := e.closestToRune(e.caret.start) - - ascent = caretStart.ascent.Ceil() - descent = caretStart.descent.Ceil() - - pos = image.Point{ - X: caretStart.x.Round(), - Y: caretStart.y, - } - pos = pos.Sub(e.scrollOff) - return -} - -// ByteOffset returns the start byte of the rune at the given -// rune offset, clamped to the size of the text. -func (e *textView) ByteOffset(runeOffset int) int64 { - return int64(e.runeOffset(e.closestToRune(runeOffset).runes)) -} - -// Len is the length of the editor contents, in runes. -func (e *textView) Len() int { - e.makeValid() - return e.closestToRune(math.MaxInt).runes -} - -// Text returns the contents of the editor. If the provided buf is large enough, it will -// be filled and returned. Otherwise a new buffer will be allocated. -// Callers can guarantee that buf is large enough by giving it capacity e.Len()*utf8.UTFMax. -func (e *textView) Text(buf []byte) []byte { - size := e.rr.Size() - if cap(buf) < int(size) { - buf = make([]byte, size) - } - buf = buf[:size] - e.Seek(0, io.SeekStart) - n, _ := io.ReadFull(e, buf) - buf = buf[:n] - return buf -} - -func (e *textView) ScrollBounds() image.Rectangle { - var b image.Rectangle - if e.SingleLine { - if len(e.index.lines) > 0 { - line := e.index.lines[0] - b.Min.X = line.xOff.Floor() - if b.Min.X > 0 { - b.Min.X = 0 - } - } - b.Max.X = e.dims.Size.X + b.Min.X - e.viewSize.X - } else { - b.Max.Y = e.dims.Size.Y - e.viewSize.Y - } - return b -} - -func (e *textView) ScrollRel(dx, dy int) { - e.scrollAbs(e.scrollOff.X+dx, e.scrollOff.Y+dy) -} - -// ScrollOff returns the scroll offset of the text viewport. -func (e *textView) ScrollOff() image.Point { - return e.scrollOff -} - -func (e *textView) scrollAbs(x, y int) { - e.scrollOff.X = x - e.scrollOff.Y = y - b := e.ScrollBounds() - if e.scrollOff.X > b.Max.X { - e.scrollOff.X = b.Max.X - } - if e.scrollOff.X < b.Min.X { - e.scrollOff.X = b.Min.X - } - if e.scrollOff.Y > b.Max.Y { - e.scrollOff.Y = b.Max.Y - } - if e.scrollOff.Y < b.Min.Y { - e.scrollOff.Y = b.Min.Y - } -} - -// MoveCoord moves the caret to the position closest to the provided -// point that is aligned to a grapheme cluster boundary. -func (e *textView) MoveCoord(pos image.Point) { - x := fixed.I(pos.X + e.scrollOff.X) - y := pos.Y + e.scrollOff.Y - e.caret.start = e.closestToXYGraphemes(x, y).runes - e.caret.xoff = 0 -} - -// Truncated returns whether the text in the textView is currently -// truncated due to a restriction on the number of lines. -func (e *textView) Truncated() bool { - return e.index.truncated -} - -func (e *textView) layoutText(lt *text.Shaper) { - e.Seek(0, io.SeekStart) - var r io.Reader = e - if e.Mask != 0 { - e.maskReader.Reset(e, e.Mask) - r = &e.maskReader - } - e.index.reset() - it := textIterator{viewport: image.Rectangle{Max: image.Point{X: math.MaxInt, Y: math.MaxInt}}} - if lt != nil { - lt.Layout(e.params, r) - for { - g, ok := lt.NextGlyph() - if !it.processGlyph(g, ok) { - break - } - e.index.Glyph(g) - } - } else { - // Make a fake glyph for every rune in the reader. - b := bufio.NewReader(r) - for _, _, err := b.ReadRune(); err != io.EOF; _, _, err = b.ReadRune() { - g := text.Glyph{Runes: 1, Flags: text.FlagClusterBreak} - _ = it.processGlyph(g, true) - e.index.Glyph(g) - } - } - e.paragraphReader.SetSource(e.rr) - e.graphemes = e.graphemes[:0] - for g := e.paragraphReader.Graphemes(); len(g) > 0; g = e.paragraphReader.Graphemes() { - if len(e.graphemes) > 0 && g[0] == e.graphemes[len(e.graphemes)-1] { - g = g[1:] - } - e.graphemes = append(e.graphemes, g...) - } - dims := layout.Dimensions{Size: it.bounds.Size()} - dims.Baseline = dims.Size.Y - it.baseline - e.dims = dims -} - -// CaretPos returns the line & column numbers of the caret. -func (e *textView) CaretPos() (line, col int) { - pos := e.closestToRune(e.caret.start) - return pos.lineCol.line, pos.lineCol.col -} - -// CaretCoords returns the coordinates of the caret, relative to the -// editor itself. -func (e *textView) CaretCoords() f32.Point { - pos := e.closestToRune(e.caret.start) - return f32.Pt(float32(pos.x)/64-float32(e.scrollOff.X), float32(pos.y-e.scrollOff.Y)) -} - -// indexRune returns the latest rune index and byte offset no later than r. -func (e *textView) indexRune(r int) offEntry { - // Initialize index. - if len(e.offIndex) == 0 { - e.offIndex = append(e.offIndex, offEntry{}) - } - i := sort.Search(len(e.offIndex), func(i int) bool { - entry := e.offIndex[i] - return entry.runes >= r - }) - // Return the entry guaranteed to be less than or equal to r. - if i > 0 { - i-- - } - return e.offIndex[i] -} - -// runeOffset returns the byte offset into e.rr of the r'th rune. -// r must be a valid rune index, usually returned by closestPosition. -func (e *textView) runeOffset(r int) int { - const runesPerIndexEntry = 50 - entry := e.indexRune(r) - lastEntry := e.offIndex[len(e.offIndex)-1].runes - for entry.runes < r { - if entry.runes > lastEntry && entry.runes%runesPerIndexEntry == runesPerIndexEntry-1 { - e.offIndex = append(e.offIndex, entry) - } - _, s, _ := e.ReadRuneAt(int64(entry.bytes)) - entry.bytes += s - entry.runes++ - } - return entry.bytes -} - -func (e *textView) invalidate() { - e.offIndex = e.offIndex[:0] - e.valid = false -} - -// Replace the text between start and end with s. Indices are in runes. -// It returns the number of runes inserted. -func (e *textView) Replace(start, end int, s string) int { - if start > end { - start, end = end, start - } - startPos := e.closestToRune(start) - endPos := e.closestToRune(end) - startOff := e.runeOffset(startPos.runes) - replaceSize := endPos.runes - startPos.runes - sc := utf8.RuneCountInString(s) - newEnd := startPos.runes + sc - - e.rr.ReplaceRunes(int64(startOff), int64(replaceSize), s) - adjust := func(pos int) int { - switch { - case newEnd < pos && pos <= endPos.runes: - pos = newEnd - case endPos.runes < pos: - diff := newEnd - endPos.runes - pos = pos + diff - } - return pos - } - e.caret.start = adjust(e.caret.start) - e.caret.end = adjust(e.caret.end) - e.invalidate() - return sc -} - -// MovePages moves the caret position by vertical pages of text, ensuring that -// the final position is aligned to a grapheme cluster boundary. -func (e *textView) MovePages(pages int, selAct selectionAction) { - caret := e.closestToRune(e.caret.start) - x := caret.x + e.caret.xoff - y := caret.y + pages*e.viewSize.Y - pos := e.closestToXYGraphemes(x, y) - e.caret.start = pos.runes - e.caret.xoff = x - pos.x - e.updateSelection(selAct) -} - -// moveByGraphemes returns the rune index resulting from moving the -// specified number of grapheme clusters from startRuneidx. -func (e *textView) moveByGraphemes(startRuneidx, graphemes int) int { - if len(e.graphemes) == 0 { - return startRuneidx - } - startGraphemeIdx, _ := slices.BinarySearch(e.graphemes, startRuneidx) - startGraphemeIdx = max(startGraphemeIdx+graphemes, 0) - startGraphemeIdx = min(startGraphemeIdx, len(e.graphemes)-1) - startRuneIdx := e.graphemes[startGraphemeIdx] - return e.closestToRune(startRuneIdx).runes -} - -// clampCursorToGraphemes ensures that the final start/end positions of -// the cursor are on grapheme cluster boundaries. -func (e *textView) clampCursorToGraphemes() { - e.caret.start = e.moveByGraphemes(e.caret.start, 0) - e.caret.end = e.moveByGraphemes(e.caret.end, 0) -} - -// MoveCaret moves the caret (aka selection start) and the selection end -// relative to their current positions. Positive distances moves forward, -// negative distances moves backward. Distances are in grapheme clusters which -// better match the expectations of users than runes. -func (e *textView) MoveCaret(startDelta, endDelta int) { - e.caret.xoff = 0 - e.caret.start = e.moveByGraphemes(e.caret.start, startDelta) - e.caret.end = e.moveByGraphemes(e.caret.end, endDelta) -} - -// MoveTextStart moves the caret to the start of the text. -func (e *textView) MoveTextStart(selAct selectionAction) { - caret := e.closestToRune(e.caret.end) - e.caret.start = 0 - e.caret.end = caret.runes - e.caret.xoff = -caret.x - e.updateSelection(selAct) - e.clampCursorToGraphemes() -} - -// MoveTextEnd moves the caret to the end of the text. -func (e *textView) MoveTextEnd(selAct selectionAction) { - caret := e.closestToRune(math.MaxInt) - e.caret.start = caret.runes - e.caret.xoff = fixed.I(e.params.MaxWidth) - caret.x - e.updateSelection(selAct) - e.clampCursorToGraphemes() -} - -// MoveLineStart moves the caret to the start of the current line, ensuring that the resulting -// cursor position is on a grapheme cluster boundary. -func (e *textView) MoveLineStart(selAct selectionAction) { - caret := e.closestToRune(e.caret.start) - caret = e.closestToLineCol(caret.lineCol.line, 0) - e.caret.start = caret.runes - e.caret.xoff = -caret.x - e.updateSelection(selAct) - e.clampCursorToGraphemes() -} - -// MoveLineEnd moves the caret to the end of the current line, ensuring that the resulting -// cursor position is on a grapheme cluster boundary. -func (e *textView) MoveLineEnd(selAct selectionAction) { - caret := e.closestToRune(e.caret.start) - caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt) - e.caret.start = caret.runes - e.caret.xoff = fixed.I(e.params.MaxWidth) - caret.x - e.updateSelection(selAct) - e.clampCursorToGraphemes() -} - -// MoveWord moves the caret to the next word in the specified direction. -// Positive is forward, negative is backward. -// Absolute values greater than one will skip that many words. -// The final caret position will be aligned to a grapheme cluster boundary. -// BUG(whereswaldon): this method's definition of a "word" is currently -// whitespace-delimited. Languages that do not use whitespace to delimit -// words will experience counter-intuitive behavior when navigating by -// word. -func (e *textView) MoveWord(distance int, selAct selectionAction) { - // split the distance information into constituent parts to be - // used independently. - words, direction := distance, 1 - if distance < 0 { - words, direction = distance*-1, -1 - } - // atEnd if caret is at either side of the buffer. - caret := e.closestToRune(e.caret.start) - atEnd := func() bool { - return caret.runes == 0 || caret.runes == e.Len() - } - // next returns the appropriate rune given the direction. - next := func() (r rune) { - off := e.runeOffset(caret.runes) - if direction < 0 { - r, _, _ = e.ReadRuneBefore(int64(off)) - } else { - r, _, _ = e.ReadRuneAt(int64(off)) - } - return r - } - for range words { - for r := next(); unicode.IsSpace(r) && !atEnd(); r = next() { - e.MoveCaret(direction, 0) - caret = e.closestToRune(e.caret.start) - } - e.MoveCaret(direction, 0) - caret = e.closestToRune(e.caret.start) - for r := next(); !unicode.IsSpace(r) && !atEnd(); r = next() { - e.MoveCaret(direction, 0) - caret = e.closestToRune(e.caret.start) - } - } - e.updateSelection(selAct) - e.clampCursorToGraphemes() -} - -func (e *textView) ScrollToCaret() { - caret := e.closestToRune(e.caret.start) - if e.SingleLine { - var dist int - if d := caret.x.Floor() - e.scrollOff.X; d < 0 { - dist = d - } else if d := caret.x.Ceil() - (e.scrollOff.X + e.viewSize.X); d > 0 { - dist = d - } - e.ScrollRel(dist, 0) - } else { - miny := caret.y - caret.ascent.Ceil() - maxy := caret.y + caret.descent.Ceil() - var dist int - if d := miny - e.scrollOff.Y; d < 0 { - dist = d - } else if d := maxy - (e.scrollOff.Y + e.viewSize.Y); d > 0 { - dist = d - } - e.ScrollRel(0, dist) - } -} - -// SelectionLen returns the length of the selection, in runes; it is -// equivalent to utf8.RuneCountInString(e.SelectedText()). -func (e *textView) SelectionLen() int { - return abs(e.caret.start - e.caret.end) -} - -// Selection returns the start and end of the selection, as rune offsets. -// start can be > end. -func (e *textView) Selection() (start, end int) { - return e.caret.start, e.caret.end -} - -// SetCaret moves the caret to start, and sets the selection end to end. Then -// the two ends are clamped to the nearest grapheme cluster boundary. start -// and end are in runes, and represent offsets into the editor text. -func (e *textView) SetCaret(start, end int) { - e.caret.start = e.closestToRune(start).runes - e.caret.end = e.closestToRune(end).runes - e.clampCursorToGraphemes() -} - -// SelectedText returns the currently selected text (if any) from the editor, -// filling the provided byte slice if it is large enough or allocating and -// returning a new byte slice if the provided one is insufficient. -// Callers can guarantee that the buf is large enough by providing a buffer -// with capacity e.SelectionLen()*utf8.UTFMax. -func (e *textView) SelectedText(buf []byte) []byte { - startOff := e.runeOffset(e.caret.start) - endOff := e.runeOffset(e.caret.end) - start := min(startOff, endOff) - end := max(startOff, endOff) - if cap(buf) < end-start { - buf = make([]byte, end-start) - } - buf = buf[:end-start] - n, _ := e.rr.ReadAt(buf, int64(start)) - // There is no way to reasonably handle a read error here. We rely upon - // implementations of textSource to provide other ways to signal errors - // if the user cares about that, and here we use whatever data we were - // able to read. - return buf[:n] -} - -func (e *textView) updateSelection(selAct selectionAction) { - if selAct == selectionClear { - e.ClearSelection() - } -} - -// ClearSelection clears the selection, by setting the selection end equal to -// the selection start. -func (e *textView) ClearSelection() { - e.caret.end = e.caret.start -} - -// WriteTo implements io.WriterTo. -func (e *textView) WriteTo(w io.Writer) (int64, error) { - e.Seek(0, io.SeekStart) - return io.Copy(w, struct{ io.Reader }{e}) -} - -// Seek implements io.Seeker. -func (e *textView) Seek(offset int64, whence int) (int64, error) { - switch whence { - case io.SeekStart: - e.seekCursor = offset - case io.SeekCurrent: - e.seekCursor += offset - case io.SeekEnd: - e.seekCursor = e.rr.Size() + offset - } - return e.seekCursor, nil -} - -// Read implements io.Reader. -func (e *textView) Read(p []byte) (int, error) { - n, err := e.rr.ReadAt(p, e.seekCursor) - e.seekCursor += int64(n) - return n, err -} - -// ReadAt implements io.ReaderAt. -func (e *textView) ReadAt(p []byte, offset int64) (int, error) { - return e.rr.ReadAt(p, offset) -} - -// Regions returns visible regions covering the rune range [start,end). -func (e *textView) Regions(start, end int, regions []Region) []Region { - viewport := image.Rectangle{ - Min: e.scrollOff, - Max: e.viewSize.Add(e.scrollOff), - } - return e.index.locate(viewport, start, end, regions) -} diff --git a/gio/widget/text_bench_test.go b/gio/widget/text_bench_test.go deleted file mode 100644 index ca8070c..0000000 --- a/gio/widget/text_bench_test.go +++ /dev/null @@ -1,353 +0,0 @@ -package widget - -import ( - "fmt" - "image" - "math/rand" - "os" - "sort" - "testing" - - colEmoji "eliasnaur.com/font/noto/emoji/color" - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/font/gofont" - "github.com/p9c/p9/pkg/gel/gio/font/opentype" - "github.com/p9c/p9/pkg/gel/gio/gpu/headless" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "golang.org/x/exp/maps" -) - -var ( - documents = map[string]string{ - "latin": latinDocument, - "arabic": arabicDocument, - "complex": complexDocument, - "emoji": emojiDocument, - } - emojiFace = func() opentype.Face { - face, _ := opentype.Parse(colEmoji.TTF) - return face - }() - sizes = []int{10, 100, 1000} - locales = []system.Locale{arabic, english} - benchFonts = func() []font.FontFace { - collection := gofont.Collection() - collection = append(collection, arabicCollection...) - collection = append(collection, font.FontFace{ - Font: font.Font{ - Typeface: "Noto Color Emoji", - }, - Face: emojiFace, - }) - return collection - }() -) - -func runBenchmarkPermutations(b *testing.B, benchmark func(b *testing.B, runes int, locale system.Locale, document string)) { - docKeys := maps.Keys(documents) - sort.Strings(docKeys) - for _, locale := range locales { - for _, runes := range sizes { - for _, textType := range docKeys { - txt := documents[textType] - b.Run(fmt.Sprintf("%drunes-%s-%s", runes, locale.Direction, textType), func(b *testing.B) { - benchmark(b, runes, locale, txt) - }) - } - } - } -} - -var render bool - -func init() { - if _, ok := os.LookupEnv("RENDER_WIDGET_TESTS"); ok { - render = true - } -} - -func BenchmarkLabelStatic(b *testing.B) { - runBenchmarkPermutations(b, func(b *testing.B, runeCount int, locale system.Locale, txt string) { - var win *headless.Window - size := image.Pt(200, 1000) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{ - Max: size, - }, - Locale: locale, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts)) - if render { - win, _ = headless.NewWindow(size.X, size.Y) - defer win.Release() - } - fontSize := unit.Sp(10) - font := font.Font{} - runes := []rune(txt)[:runeCount] - runesStr := string(runes) - l := Label{} - b.ResetTimer() - for b.Loop() { - l.Layout(gtx, cache, font, fontSize, runesStr, op.CallOp{}) - if render { - win.Frame(gtx.Ops) - } - gtx.Ops.Reset() - } - }) -} - -func BenchmarkLabelDynamic(b *testing.B) { - runBenchmarkPermutations(b, func(b *testing.B, runeCount int, locale system.Locale, txt string) { - var win *headless.Window - size := image.Pt(200, 1000) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{ - Max: size, - }, - Locale: locale, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts)) - if render { - win, _ = headless.NewWindow(size.X, size.Y) - defer win.Release() - } - fontSize := unit.Sp(10) - font := font.Font{} - runes := []rune(txt)[:runeCount] - l := Label{} - r := rand.New(rand.NewSource(42)) - b.ResetTimer() - for b.Loop() { - // simulate a constantly changing string - a := r.Intn(len(runes)) - b := r.Intn(len(runes)) - runes[a], runes[b] = runes[b], runes[a] - l.Layout(gtx, cache, font, fontSize, string(runes), op.CallOp{}) - if render { - win.Frame(gtx.Ops) - } - gtx.Ops.Reset() - } - }) -} - -func BenchmarkEditorStatic(b *testing.B) { - runBenchmarkPermutations(b, func(b *testing.B, runeCount int, locale system.Locale, txt string) { - var win *headless.Window - size := image.Pt(200, 1000) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{ - Max: size, - }, - Locale: locale, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts)) - if render { - win, _ = headless.NewWindow(size.X, size.Y) - defer win.Release() - } - fontSize := unit.Sp(10) - font := font.Font{} - runes := []rune(txt)[:runeCount] - runesStr := string(runes) - e := Editor{} - e.SetText(runesStr) - b.ResetTimer() - for b.Loop() { - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - if render { - win.Frame(gtx.Ops) - } - gtx.Ops.Reset() - } - }) -} - -func BenchmarkEditorDynamic(b *testing.B) { - runBenchmarkPermutations(b, func(b *testing.B, runeCount int, locale system.Locale, txt string) { - var win *headless.Window - size := image.Pt(200, 1000) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{ - Max: size, - }, - Locale: locale, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts)) - if render { - win, _ = headless.NewWindow(size.X, size.Y) - defer win.Release() - } - fontSize := unit.Sp(10) - font := font.Font{} - runes := []rune(txt)[:runeCount] - e := Editor{} - e.SetText(string(runes)) - r := rand.New(rand.NewSource(42)) - b.ResetTimer() - for b.Loop() { - // simulate a constantly changing string - a := r.Intn(e.Len()) - b := r.Intn(e.Len()) - e.SetCaret(a, a+1) - takeStr := e.SelectedText() - e.Insert("") - e.SetCaret(b, b) - e.Insert(takeStr) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - if render { - win.Frame(gtx.Ops) - } - gtx.Ops.Reset() - } - }) -} - -func FuzzEditorEditing(f *testing.F) { - f.Add(complexDocument, int16(0), int16(len([]rune(complexDocument)))) - gtx := layout.Context{ - Ops: new(op.Ops), - Constraints: layout.Constraints{ - Max: image.Pt(200, 1000), - }, - Locale: arabic, - } - cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(benchFonts)) - fontSize := unit.Sp(10) - font := font.Font{} - e := Editor{} - f.Fuzz(func(t *testing.T, txt string, replaceFrom, replaceTo int16) { - e.SetText(txt) - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - // simulate a constantly changing string - if e.Len() > 0 { - a := int(replaceFrom) % e.Len() - b := int(replaceTo) % e.Len() - e.SetCaret(a, a+1) - takeStr := e.SelectedText() - e.Insert("") - e.SetCaret(b, b) - e.Insert(takeStr) - } - e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) - gtx.Ops.Reset() - }) -} - -const ( - latinDocument = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. -Porttitor eget dolor morbi non arcu risus quis. -Nibh sit amet commodo nulla. -Posuere ac ut consequat semper viverra nam libero justo. -Risus in hendrerit gravida rutrum quisque. -Natoque penatibus et magnis dis parturient montes nascetur. -In metus vulputate eu scelerisque felis imperdiet proin fermentum. -Mattis rhoncus urna neque viverra. -Elit pellentesque habitant morbi tristique. -Nisl nunc mi ipsum faucibus vitae aliquet nec. -Sed augue lacus viverra vitae congue eu consequat. -At quis risus sed vulputate odio ut. -Sit amet volutpat consequat mauris nunc congue nisi. -Dignissim cras tincidunt lobortis feugiat. -Faucibus turpis in eu mi bibendum. -Odio aenean sed adipiscing diam donec adipiscing tristique. -Fermentum leo vel orci porta non pulvinar. -Ut venenatis tellus in metus vulputate eu scelerisque felis imperdiet. -Et netus et malesuada fames ac turpis. -Venenatis urna cursus eget nunc scelerisque viverra mauris in. -Risus ultricies tristique nulla aliquet enim tortor. -Risus pretium quam vulputate dignissim suspendisse in. -Interdum velit euismod in pellentesque massa placerat duis ultricies lacus. -Proin gravida hendrerit lectus a. -Auctor augue mauris augue neque gravida in fermentum et. -Laoreet sit amet cursus sit amet dictum. -In fermentum et sollicitudin ac orci phasellus egestas tellus rutrum. -Tempus imperdiet nulla malesuada pellentesque elit eget gravida. -Consequat id porta nibh venenatis cras sed. -Vulputate ut pharetra sit amet aliquam. -Congue mauris rhoncus aenean vel elit. -Risus quis varius quam quisque id diam vel quam elementum. -Pretium lectus quam id leo in vitae. -Sed sed risus pretium quam vulputate dignissim suspendisse in est. -Velit laoreet id donec ultrices. -Nunc sed velit dignissim sodales ut. -Nunc scelerisque viverra mauris in aliquam sem fringilla ut. -Sed enim ut sem viverra aliquet eget sit. -Convallis posuere morbi leo urna molestie at. -Aliquam id diam maecenas ultricies mi eget mauris. -Ipsum dolor sit amet consectetur adipiscing elit ut aliquam. -Accumsan tortor posuere ac ut consequat semper. -Viverra vitae congue eu consequat ac felis donec et odio. -Scelerisque in dictum non consectetur a. -Consequat nisl vel pretium lectus quam id leo in vitae. -Morbi tristique senectus et netus et malesuada fames ac turpis. -Ac orci phasellus egestas tellus. -Tempus egestas sed sed risus. -Ullamcorper morbi tincidunt ornare massa eget egestas purus. -Nibh venenatis cras sed felis eget velit.` - arabicDocument = `و سأعرض مثال حي لهذا، من منا لم يتحمل جهد بدني شاق إلا من أجل الحصول على ميزة أو فائدة؟ ولكن من لديه الحق أن ينتقد شخص ما أراد أن يشعر بالسعادة التي لا تشوبها عواقب أليمة أو آخر أراد أن يتجنب الألم الذي ربما تنجم عنه بعض المتعة ؟ علي الجانب الآخر نشجب ونستنكر هؤلاء الرجال المفتونون بنشوة اللحظة الهائمون في رغباتهم فلا يدركون ما يعقبها من الألم والأسي المحتم، واللوم كذلك يشمل هؤلاء الذين أخفقوا في واجباتهم نتيجة لضعف إرادتهم فيتساوي مع هؤلاء الذين يتجنبون وينأون عن تحمل الكدح والألم . -من المفترض أن نفرق بين هذه الحالات بكل سهولة ومرونة. -في ذاك الوقت عندما تكون قدرتنا علي الاختيار غير مقيدة بشرط وعندما لا نجد ما يمنعنا أن نفعل الأفضل فها نحن نرحب بالسرور والسعادة ونتجنب كل ما يبعث إلينا الألم. -في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور بالسرور ونقبل ما يجلبه إلينا الأسى. -الإنسان الحكيم عليه أن يمسك زمام الأمور ويختار إما أن يرفض مصادر السعادة من أجل ما هو أكثر أهمية أو يتحمل الألم من أجل ألا يتحمل ما هو أسوأ. -و سأعرض مثال حي لهذا، من منا لم يتحمل جهد بدني شاق إلا من أجل الحصول على ميزة أو فائدة؟ ولكن من لديه الحق أن ينتقد شخص ما أراد أن يشعر بالسعادة التي لا تشوبها عواقب أليمة أو آخر أراد أن يتجنب الألم الذي ربما تنجم عنه بعض المتعة ؟ علي الجانب الآخر نشجب ونستنكر هؤلاء الرجال المفتونون بنشوة اللحظة الهائمون في رغباتهم فلا يدركون ما يعقبها من الألم والأسي المحتم، واللوم كذلك يشمل هؤلاء الذين أخفقوا في واجباتهم نتيجة لضعف إرادتهم فيتساوي مع هؤلاء الذين يتجنبون وينأون عن تحمل الكدح والألم . -من المفترض أن نفرق بين هذه الحالات بكل سهولة ومرونة. -في ذاك الوقت عندما تكون قدرتنا علي الاختيار غير مقيدة بشرط وعندما لا نجد ما يمنعنا أن نفعل الأفضل فها نحن نرحب بالسرور والسعادة ونتجنب كل ما يبعث إلينا الألم. -في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور بالسرور ونقبل ما يجلبه إلينا الأسى. -الإنسان الحكيم عليه أن يمسك زمام الأمور ويختار إما أن يرفض مصادر السعادة من أجل ما هو أكثر أهمية أو يتحمل الألم من أجل ألا يتحمل ما هو أسوأ.` - complexDocument = `و سأعرض مثال dolor sit amet, لم يتحمل جهد adipiscing elit, sed do الحصول على ميزة incididunt ut labore أن ينتقد magna aliqua. -Porttitor إرادتهم فيتساوي morbi non arcu يدركون ما يعقبها . -Nibh نشجب ونستنكر commodo nulla. -بكل سهولة ومرونة ut consequat لهذا، من منا nam libero justo. -Risus in hendrerit علينا الواجب والعمل. -Natoque تكون قدرتنا علي magnis dis parturient يمسك زمام الأمور ويختار. -In نجد ما يمنعنا eu scelerisque ونظراً للالتزامات التي fermentum. -Mattis ة بشرط وعندما لا neque viverra. -يمسك زمام الأمور habitant لهذا، من. -Nisl تي يفرضها علينا faucibus ،من منا لم nec. -Sed augue علي الاختيار غير vitae congue eu consequat. -At quis risus سك زمام الأمور ويختار. -Sit amet volutpat consequat mauris الأمور ويختار إما nisi. -Dignissim لواجب والعمل tincidunt سنتنازل feugiat. -Faucibus التزامات in eu mi bibendum. -Odio ويختار إما أن يرفض مصادر السعادة sed adipiscing ذا، من منا لم tristique. -Fermentum leo vel ور ويختار إما pulvinar. -Ut ر إما أن يرفض مصادر السعادة من in metus تكون قدرتنا علي felis imperdiet. -ي الاختيار غير مقيدة بشرط et malesuada fames ac turpis. -Venenatis على ميزة أو فائدة؟ ولكن eget nunc scelerisque سك زمام الأمور ويختار إما in. -رتنا ultricies tristique ي الاختيار غير مقيدة بشرط enim tortor. -Risus اختيار غير مقيدة بشرط وعندما quam سان الحكيم عليه أن suspendisse in. -Interdum velit ونظراً للالتزامات التي pellentesque massa placerat لأمور ويختار إما أن يرفض lacus. -Proin دما تكون قدرتنا علي الاختيار lectus a. -Auctor الوقت عندما تكون augue neque ض مثال حي fermentum et. -Laoreet مسك زمام الأمور ويختار amet cursus لم يتحمل جهد dictum. -In fermentum et sollicitudin ac orci phasellus علي الاختيار غير rutrum. -Tempus imperdiet المفترض أن نفرق pellentesque ت بكل سهولة eget gravida. -Consequat id portaمصادر السعادة cras sed. -Vulputate علي الاختيار غير مقيدة sit amet aliquam. -Congue mauris حيان ونظراً للالتزامات التي vel elit. -Risus quis varius quam quisque id ار غير مقيدة بشرط elementum. -Pretium تي يفرضها علينا الواجب leo in vitae. - شاق إلا من أجل pretium quam الحكيم عليه أن يمسك suspendisse in est. -Velit ونظراً للالتزامات التي يفرضها ultrices. - الوقت عندما تكون velit dignissim يه أن يمسك . -Nunc scelerisque viverra mauris in aliquam sem ر إما أن ut. -السعادة من أجل ما هو أكثر أهمية أو يتحمل الألم -Convallis posuere morbi leo urna molestie at.` - emojiDocument = `📚🎶🐰🌷👹🌟 🔰🐲📑🍢🔎 👢💮👷👧💑🐪 📙📜🐎🏠🎠 👧🌼💛🎉💜🎍 🔜💷🐉👘🕟📗🍟 🎆🍚📹💄 🐾🎩💽👘 📒💕👅💽🐩 📷🌌🌚🎣📌. 🍈🍅🔖🍄 🍐🔈🍤🐽 🐹💘🍚👩📡 🎸🏠🔳🏩🌳💣 🔡🔠🕤🔔🎴📕 📼👝🎓🕗💸 📓🌽🍟💵🕗🌒🏉📨 🔀🏉🍴💘🍣💸 🔪🔻🕖🎰 🐲👮🔙🌇🐒🏇 🐝🌚🏫🔀👍 👾🎧🍋🍔👧 💣💞🐴👆🐢🏊📀 🕤🌃🍌🕛🔬. 🏃🍜🍔🐽🎁🏩🎰 📮🍄🐖💕👈 🔠🕡🐊💞🍬📳 🎤🌆🌛🐍🔳 🐄🔇🔱🌇📺👞 💌👍📳🎤🏂 👞🎉🍶📊🔶🌅🐭🕙 🍜📠🎴💒🔶 📀💂🌷👺👙. - -📥🕝🎎🐻💘🍇🔤 💠🎇📦👩🍁 👜🍏🔏👎 🔟🌹🌗🎬🔙 🐁📛🐝🐏🐣 🔃🗻🔎🌺👀 📰📮🏩👯🐳🍀🍇 🍨📵🌂📌 👌📐🏨🐉 🍏🍘🔟🎣🔏📠 👤📭🐱📣. 🕓👶🎳📭🔌📃🔧 📟🔰🌂🎈🔣 🔤👍🍤👔🐪 🔨🎼🎊🎪🕝🐬 📴🎶🔈🔐🔘 🐬🐯🕜🎎👴🎃 🎑🐾👏👇🔭 🐥🔙💦🔩🔮 👊🐶👗📕 🐎📹👠🍤 🔢💘📷🐷🐂 🐫💕🕕🍖🔆🎽 👼🎶🌸👻🔷🌰 🔔💉💱🔂👵🔑. 🌁🎪🎌🍘🍏 🌛🍂🔎🕃📧👻 🎍🌔🐦🐻 🔉🎌🌘💉👒 📙💠🔙📰 🌒👏💪🌇💈 🌌📯📂🌀🔁 🏧💷🍀🐐🏈 📢🌏🔷💭 👋🕓🌓🕛🏢👡👋 🍶🐂🍠🔟 👵🏇🔶🕜👎. 👹💉🔌🍳🕗 🐫🌈🔠🐀🎩🎽 👺🔣🔂👪👴🐚🕙 👀🕓🔱🌇 🎻🐘🔐🕕 🌉🔡🐊🍮 💫🎆🎹🐍🎯 🐑🐱🍠🕑🍒. - -🎳🐎🔹🎾🐹📖 📘🐒📷🕧🔛 🐾📺🎿🍖💂🕥 🍜🎷🍣👳 🕛📧💶🌑 🌀💣🎎🐛🎪🐒 🎇🌹👺🎆💄📚 🔓🍗📓🎂🌍🌘📢 🍩💞🏂💥🔹📇💴 🐇🕝💹🐣💔🎫 👐🍼🏰🎄🎨👚 👑🔗🍅🐈 🐰🐙🌻👹👆 👬🐧🍬🕡🐽💉 🌅🔉🎤🔁📨🔧🔀🍏 🎼🔛📉🌺. 👖🌔🍢🏂💯 🏁🐰🍉📬🍖 📨💜📮🔕🎣🔩 🔏🕀🏫🎳📵👭👟 💨💃📶🎃 📚🔇🍛👽🐍🐄 🔼👻🍮🍔🍨🏪🐺 📩📜🍨📖 🏢🎉🔢🌚🌀🔊💍 🐟🕚🔴🎿🍞 🌈📤👲🌿🌅🍲📛💍 🐦🔰🐗🐆🎻 👑🕐📔🎁🍙🔪🔭. 🎐🐵🎼🌒🎰🍳🎽 🐻🔉💺🕁🍷 🐛🍬💦📶🔖 🔕🌳💃🌺🔢 💒📒🔘🐸👩 🌺🍈🌀🏁🎢🔖 📈🎸🐖👪 🐅🏁🔹🎬🍖📊🗼 🎬📅💝📀🎐. 🌗📍👇🎠 🌸🐸🐐🍕🐋 💈🌌🐶💤🌻🐞 🍯🌳📌🍮🐻🍝 🕦📯🔱👒 💖🌱🐨🎰🏭🏈 🔳🏩🌟🔭📢📒 🔅💬💓💻💁💂 🔗🍂🏇🌒🌂💩🕢 🔙🌆💞📜🔘👇 🍎🌃🔢🌵🏬 🔄💢🍨📋💇🌄 🍝🍧💂🏮🏁. 🎬🐽🔇🎣🌜🔣 🌍🔒👿🎆🌞🍇🍸 👖🍘🏡🕣 📝🐖💆🎈 👙🔳👙🔩👀🔂 🐤📈💃👗🔌🎾🔭🍴 🌺👛🌵🌕🐺 🎆💼👌👘🍈👛 🎳🐪🕧🏄 💯🍟💂👖🎍 🕀💟🌷💕🐉🐲🎷. 🎍👂📓🌽 🐉🕕🐤🌲📟🔂💷 🎑📛🕠🔹🐚 🍆📹🐚🐵🏇🏢 🍠💱🕦💙🏢🐌🎎 🐄🍨📄🌾🎻🏈 🏇🍪💸🔆💍📢👢 💇🌋👝🕜🌍🐶🎓 🍪📄🐤🎃💖 🔲🕒🍧🌎🐪🌶 🍓👲🔭🍯🌔👌 🔼🐗🗼🍂 🔶🍯🎶🐅🐂💗🐴🐶 📭📰📔👬🏯🕟🐄🍊 💆👞📆🐶🌖🎁👺 🐃💺👊🌿🎌. - -🍧🕔👆🔭🕛👇 🐆🔖🎂🐭📗🗼🐐 🌐🎢🌞💛🐚 🌿🎶💎💬🔩 💾🔐🎷🍙🐬🕐 🌏🍄🎾🐎🌽🍓🐳 💥🍎👳📫🐤📼🎾 👨🕃🕞🍯🍲. 💥🎍🔉🎈👻🔵🏬🔸 🔼🍹🔱🔮🕔 🌈💎👜📠 👢🍻🍢🎃👺🌍👰 🍵👃🕠🍎🍑 📜💥📘📌 🔹🔵🍷👅💏 💮💘🐜📠👬📖 🌅🍺🔇🌈👒🔀 🎢🌆💌🍬📱🎰 🌺🍆🔰🏁🍁🎠 🔇🔁🌹🔞🎀🎬🐭🌹 🏬📫🗾🎻📌. 🐠🏣👋👊🐟 👲🔣💻👅🎎 🎇🌲🕑🍨📯 🐜📵💙📷🎒🕔 🎇🏀🔴🐑🌗 🎧🔡👅🕁🏉👛🐬 🕧🐞🎩📓🍆📪 🐼📻👼🌄 🌟🌺🏦🍧🍕🐯 🕕🕦🐤💆🍧💩 🐑📜👏👐🐧🍞👵 👞🌲🍼🔍 🌛🐔🌄🎸🐯.` -) diff --git a/gio/widget/widget_test.go b/gio/widget/widget_test.go deleted file mode 100644 index 8d588a4..0000000 --- a/gio/widget/widget_test.go +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package widget_test - -import ( - "image" - "testing" - - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/io/input" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - "github.com/p9c/p9/pkg/gel/gio/io/semantic" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/widget" -) - -func TestBool(t *testing.T) { - var ( - r input.Router - b widget.Bool - ) - gtx := layout.Context{ - Ops: new(op.Ops), - Source: r.Source(), - } - layout := func() { - b.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - semantic.CheckBox.Add(gtx.Ops) - semantic.DescriptionOp("description").Add(gtx.Ops) - return layout.Dimensions{Size: image.Pt(100, 100)} - }) - } - layout() - r.Frame(gtx.Ops) - r.Queue( - pointer.Event{ - Source: pointer.Touch, - Kind: pointer.Press, - Position: f32.Pt(50, 50), - }, - pointer.Event{ - Source: pointer.Touch, - Kind: pointer.Release, - Position: f32.Pt(50, 50), - }, - ) - gtx.Reset() - layout() - r.Frame(gtx.Ops) - tree := r.AppendSemantics(nil) - n := tree[0].Children[0].Desc - if n.Description != "description" { - t.Errorf("unexpected semantic description: %s", n.Description) - } - if n.Class != semantic.CheckBox { - t.Errorf("unexpected semantic class: %v", n.Class) - } - if !b.Value || !n.Selected { - t.Error("click did not select") - } -} diff --git a/glyphiter.go b/glyphiter.go index 50af14a..a10b394 100644 --- a/glyphiter.go +++ b/glyphiter.go @@ -3,12 +3,12 @@ package gel import ( "image" - "github.com/p9c/p9/pkg/gel/gio/f32" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" + "gioui.org/f32" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/text" "golang.org/x/image/math/fixed" ) diff --git a/glyphiter_test.go b/glyphiter_test.go index 7b50f22..c506ef4 100644 --- a/glyphiter_test.go +++ b/glyphiter_test.go @@ -4,7 +4,7 @@ import ( "image" "testing" - "github.com/p9c/p9/pkg/gel/gio/text" + "gioui.org/text" "golang.org/x/image/math/fixed" ) diff --git a/go.mod b/go.mod index 0fd6b33..b3b08f4 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,23 @@ module git.mleku.dev/mleku/prevara go 1.25.3 + +require ( + gioui.org v0.9.0 + github.com/BurntSushi/xgb v0.0.0-20210121224620-deaf085860bc + github.com/atotto/clipboard v0.1.4 + github.com/davecgh/go-spew v1.1.1 + github.com/gookit/color v1.6.0 + github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 + go.uber.org/atomic v1.11.0 + golang.org/x/exp/shiny v0.0.0-20251125195548-87e1e737ad39 + golang.org/x/image v0.33.0 +) + +require ( + gioui.org/shader v1.0.8 // indirect + github.com/go-text/typesetting v0.3.1 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect +) diff --git a/helpers.go b/helpers.go index 2fc743e..d729907 100644 --- a/helpers.go +++ b/helpers.go @@ -9,10 +9,10 @@ import ( "strconv" "strings" - "github.com/p9c/p9/pkg/gel/gio/f32" - "github.com/p9c/p9/pkg/gel/gio/font" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/unit" + "gioui.org/f32" + "gioui.org/font" + l "gioui.org/layout" + "gioui.org/unit" ) // Defining these as types gives flexibility later to create methods that modify them @@ -36,19 +36,19 @@ const Inf = 1e6 // Scales contains text scale factors for different heading levels // Based on Material Design type scale var Scales = map[string]float32{ - "H1": 6.0, - "H2": 3.75, - "H3": 3.0, - "H4": 2.125, - "H5": 1.5, - "H6": 1.25, + "H1": 6.0, + "H2": 3.75, + "H3": 3.0, + "H4": 2.125, + "H5": 1.5, + "H6": 1.25, "Subtitle1": 1.0, "Subtitle2": 0.875, - "Body1": 1.0, - "Body2": 0.875, - "Button": 0.875, - "Caption": 0.75, - "Overline": 0.625, + "Body1": 1.0, + "Body2": 0.875, + "Button": 0.875, + "Caption": 0.75, + "Overline": 0.625, } // Fill is a general fill function that covers the background of the current context space @@ -92,14 +92,14 @@ func axisMain(a l.Axis, sz image.Point) int { if a == l.Horizontal { return sz.X } - return sz.Y + return sz.Y } func axisCross(a l.Axis, sz image.Point) int { if a == l.Horizontal { return sz.Y } - return sz.X + return sz.X } func axisMainConstraint(a l.Axis, cs l.Constraints) (int, int) { diff --git a/icon.go b/icon.go index e61cfcd..300c622 100644 --- a/icon.go +++ b/icon.go @@ -5,9 +5,9 @@ import ( "image/color" "image/draw" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/op/paint" + "gioui.org/unit" "golang.org/x/exp/shiny/iconvg" ) diff --git a/iconbutton.go b/iconbutton.go index 681cdd8..cbb51a2 100644 --- a/iconbutton.go +++ b/iconbutton.go @@ -3,12 +3,12 @@ package gel import ( "image" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/unit" "golang.org/x/exp/shiny/materialdesign/icons" - "github.com/p9c/p9/pkg/gel/f32color" + "git.mleku.dev/mleku/prevara/f32color" ) type IconButton struct { diff --git a/icons/icongen/logmain.go b/icons/icongen/logmain.go index 58579ab..cc438ec 100644 --- a/icons/icongen/logmain.go +++ b/icons/icongen/logmain.go @@ -1,9 +1,9 @@ package main import ( - "github.com/p9c/p9/pkg/log" + "git.mleku.dev/mleku/prevara/pkg/log" - "github.com/p9c/p9/version" + "git.mleku.dev/mleku/prevara/version" ) var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/icons/icongen/main.go b/icons/icongen/main.go index 176adde..a74b3f9 100644 --- a/icons/icongen/main.go +++ b/icons/icongen/main.go @@ -6,7 +6,7 @@ import ( "net/http" "strings" - "github.com/p9c/p9/pkg/apputil" + "git.mleku.dev/mleku/prevara/pkg/apputil" ) func main() { @@ -46,7 +46,7 @@ func getIcons() (iconNames []string) { func getSourceCode(packagename string, iconNames []string) []byte { o := `// Package icons bundles the entire set of several icon sets into one package as maps to allow iteration -`+`//go:generate go run ./icongen/. +` + `//go:generate go run ./icongen/. package ` + packagename + ` diff --git a/image.go b/image.go index 05110b2..c2de21a 100644 --- a/image.go +++ b/image.go @@ -5,10 +5,10 @@ package gel import ( "image" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" ) // Image is a widget that displays an image. diff --git a/incdec.go b/incdec.go index bade9f8..ce95479 100644 --- a/incdec.go +++ b/incdec.go @@ -2,8 +2,8 @@ package gel import ( "fmt" - - l "github.com/p9c/p9/pkg/gel/gio/layout" + + l "gioui.org/layout" "golang.org/x/exp/shiny/materialdesign/icons" ) diff --git a/indefinite.go b/indefinite.go index 84a2320..e98b12d 100644 --- a/indefinite.go +++ b/indefinite.go @@ -6,12 +6,12 @@ import ( "math" "time" - "github.com/p9c/p9/pkg/gel/gio/f32" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + "gioui.org/f32" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" ) type Indefinite struct { diff --git a/input.go b/input.go index 18fdf48..464a003 100644 --- a/input.go +++ b/input.go @@ -5,7 +5,7 @@ import ( "golang.org/x/exp/shiny/materialdesign/icons" - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" ) type Input struct { diff --git a/inset.go b/inset.go index 80f1c83..1953936 100644 --- a/inset.go +++ b/inset.go @@ -1,8 +1,8 @@ package gel import ( - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/unit" ) type Inset struct { diff --git a/intslider.go b/intslider.go index c763907..3e3431a 100644 --- a/intslider.go +++ b/intslider.go @@ -3,7 +3,7 @@ package gel import ( "fmt" - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" ) type IntSlider struct { @@ -53,34 +53,46 @@ func (i *IntSlider) GetValue() int { func (i *IntSlider) Fn(gtx l.Context) l.Dimensions { return i.Flex().Rigid( i.Button( - i.min.SetClick(func() { - i.floater.SetValue(i.minV) - i.hook(int(i.minV)) - })). + i.min.SetClick( + func() { + i.floater.SetValue(i.minV) + i.hook(int(i.minV)) + }, + ), + ). Inset(0.25). Color("Primary"). Background("Transparent"). Font("bariol regular"). Text("0"). Fn, - ).Flexed(1, - i.Inset(0.25, + ).Flexed( + 1, + i.Inset( + 0.25, i.Slider(). - Float(i.floater.SetHook(func(fl float32) { - iFl := int(fl + 0.5) - i.value = iFl - i.floater.SetValue(float32(iFl)) - i.hook(iFl) - })). + Float( + i.floater.SetHook( + func(fl float32) { + iFl := int(fl + 0.5) + i.value = iFl + i.floater.SetValue(float32(iFl)) + i.hook(iFl) + }, + ), + ). Min(i.minV).Max(i.maxV). Fn, ).Fn, ).Rigid( i.Button( - i.max.SetClick(func() { - i.floater.SetValue(i.maxV) - i.hook(int(i.maxV)) - })). + i.max.SetClick( + func() { + i.floater.SetValue(i.maxV) + i.hook(int(i.maxV)) + }, + ), + ). Inset(0.25). Color("Primary"). Background("Transparent"). diff --git a/label.go b/label.go index a16c2b0..cc01284 100644 --- a/label.go +++ b/label.go @@ -4,13 +4,13 @@ import ( "image" "image/color" - "github.com/p9c/p9/pkg/gel/gio/font" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" + "gioui.org/font" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/text" + "gioui.org/unit" "golang.org/x/image/math/fixed" ) diff --git a/list.go b/list.go index 50db75a..97a74ac 100644 --- a/list.go +++ b/list.go @@ -3,12 +3,12 @@ package gel import ( "image" "time" - - "github.com/p9c/p9/pkg/gel/gio/gesture" - "github.com/p9c/p9/pkg/gel/gio/io/pointer" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" + + "gioui.org/gesture" + "gioui.org/io/pointer" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" ) type scrollChild struct { @@ -36,7 +36,7 @@ type List struct { maxSize int children []scrollChild dir iterationDir - + // all below are additional fields to implement the scrollbar *Window // we store the constraints here instead of in the `cs` field @@ -335,7 +335,7 @@ func (li *List) layout(macro op.MacroOp) l.Dimensions { return l.Dimensions{Size: dims} } -// Everything below is extensions on the original from github.com/p9c/p9/pkg/gel/gio/layout +// Everything below is extensions on the original from git.mleku.dev/mleku/prevara/gio/layout // Position returns the current position of the scroller func (li *List) Position() Position { diff --git a/log.go b/log.go index 1e21ce2..d245189 100644 --- a/log.go +++ b/log.go @@ -1,9 +1,9 @@ package gel import ( - "github.com/p9c/p9/pkg/log" + "git.mleku.dev/mleku/prevara/pkg/log" - "github.com/p9c/p9/version" + "git.mleku.dev/mleku/prevara/version" ) var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/multi.go b/multi.go index 89af9fc..ae7cc0b 100644 --- a/multi.go +++ b/multi.go @@ -1,7 +1,7 @@ package gel import ( - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" "golang.org/x/exp/shiny/materialdesign/icons" ) @@ -366,7 +366,7 @@ func (m *Multi) Widgets() (widgets []l.Widget) { if i == m.inputLocation { // x := i // D.Ln("rendering editor", x) - + input := func(gtx l.Context) l.Dimensions { return m.Inset(0.25, m.Flex(). diff --git a/password.go b/password.go index e7d4655..9b9970d 100644 --- a/password.go +++ b/password.go @@ -1,10 +1,10 @@ package gel import ( - "github.com/p9c/p9/pkg/opts/text" + "git.mleku.dev/mleku/prevara/pkg/opts/text" icons2 "golang.org/x/exp/shiny/materialdesign/icons" - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" ) type Password struct { diff --git a/poolgen/log.go b/poolgen/log.go index 58579ab..cc438ec 100644 --- a/poolgen/log.go +++ b/poolgen/log.go @@ -1,9 +1,9 @@ package main import ( - "github.com/p9c/p9/pkg/log" + "git.mleku.dev/mleku/prevara/pkg/log" - "github.com/p9c/p9/version" + "git.mleku.dev/mleku/prevara/version" ) var F, E, W, I, D, T = log.GetLogPrinterSet(log.AddLoggerSubsystem(version.PathBase)) diff --git a/poolgen/poolgen.go b/poolgen/poolgen.go index 218cb1c..c423777 100644 --- a/poolgen/poolgen.go +++ b/poolgen/poolgen.go @@ -19,7 +19,7 @@ var types = []poolType{ func main() { var out string - out += `// generated by go run github.com/p9c/p9/pkg/gel/poolgen/poolgen.go; DO NOT EDIT + out += `// generated by go run git.mleku.dev/mleku/prevara/poolgen/poolgen.go; DO NOT EDIT // ` + `//go:generate go run ./poolgen/. diff --git a/pooltypes.go b/pooltypes.go index d717874..d565f16 100644 --- a/pooltypes.go +++ b/pooltypes.go @@ -1,4 +1,4 @@ -// generated by go run github.com/p9c/p9/pkg/gel/poolgen/poolgen.go; DO NOT EDIT +// generated by go run git.mleku.dev/mleku/prevara/poolgen/poolgen.go; DO NOT EDIT // //go:generate go run ./poolgen/. @@ -35,7 +35,6 @@ func (p *Pool) FreeBool(b *Bool) { } } - func (p *Pool) GetList() (out *List) { if p.listsInUse >= len(p.lists) { // Pre-allocate capacity to avoid repeated growth @@ -67,7 +66,6 @@ func (p *Pool) FreeList(b *List) { } } - func (p *Pool) GetCheckable() (out *Checkable) { if p.checkablesInUse >= len(p.checkables) { // Pre-allocate capacity to avoid repeated growth @@ -99,7 +97,6 @@ func (p *Pool) FreeCheckable(b *Checkable) { } } - func (p *Pool) GetClickable() (out *Clickable) { if p.clickablesInUse >= len(p.clickables) { // Pre-allocate capacity to avoid repeated growth @@ -131,7 +128,6 @@ func (p *Pool) FreeClickable(b *Clickable) { } } - func (p *Pool) GetEditor() (out *Editor) { if p.editorsInUse >= len(p.editors) { // Pre-allocate capacity to avoid repeated growth @@ -163,7 +159,6 @@ func (p *Pool) FreeEditor(b *Editor) { } } - func (p *Pool) GetIncDec() (out *IncDec) { if p.incDecsInUse >= len(p.incDecs) { // Pre-allocate capacity to avoid repeated growth @@ -194,4 +189,3 @@ func (p *Pool) FreeIncDec(b *IncDec) { } } } - diff --git a/progressbar.go b/progressbar.go index 5d51a02..bec2f03 100644 --- a/progressbar.go +++ b/progressbar.go @@ -4,12 +4,12 @@ import ( "image" "image/color" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" - "github.com/p9c/p9/pkg/gel/f32color" + "git.mleku.dev/mleku/prevara/f32color" ) type ProgressBar struct { diff --git a/radiobutton.go b/radiobutton.go index e9148ac..7ccb6ad 100644 --- a/radiobutton.go +++ b/radiobutton.go @@ -5,7 +5,7 @@ package gel import ( "golang.org/x/exp/shiny/materialdesign/icons" - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" ) type RadioButton struct { @@ -26,9 +26,9 @@ func (w *Window) RadioButton(checkable *Checkable, group *Enum, key, group: group, Window: w, Checkable: checkable. - CheckedStateIcon(&icons.ToggleRadioButtonChecked). // Color("Primary"). + CheckedStateIcon(&icons.ToggleRadioButtonChecked). // Color("Primary"). UncheckedStateIcon(&icons.ToggleRadioButtonUnchecked). // Color("PanelBg"). - Label(label), // .Color("DocText").IconColor("PanelBg"), + Label(label), // .Color("DocText").IconColor("PanelBg"), key: key, } } diff --git a/responsive.go b/responsive.go index 477b634..7a9bb8a 100644 --- a/responsive.go +++ b/responsive.go @@ -2,8 +2,8 @@ package gel import ( "sort" - - l "github.com/p9c/p9/pkg/gel/gio/layout" + + l "gioui.org/layout" ) // WidgetSize is a widget with a specification of the minimum size to select it for viewing. diff --git a/slider.go b/slider.go index 6618beb..3b80cc8 100644 --- a/slider.go +++ b/slider.go @@ -4,13 +4,13 @@ import ( "image" "image/color" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" - "github.com/p9c/p9/pkg/gel/f32color" + "git.mleku.dev/mleku/prevara/f32color" ) type Slider struct { diff --git a/stack.go b/stack.go index 0c26abf..5c26ba2 100644 --- a/stack.go +++ b/stack.go @@ -1,6 +1,6 @@ package gel -import l "github.com/p9c/p9/pkg/gel/gio/layout" +import l "gioui.org/layout" type Stack struct { *l.Stack diff --git a/switch.go b/switch.go index e0440cd..1afa2e0 100644 --- a/switch.go +++ b/switch.go @@ -4,13 +4,13 @@ import ( "image" "image/color" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" - "github.com/p9c/p9/pkg/gel/f32color" + "git.mleku.dev/mleku/prevara/f32color" ) type Switch struct { diff --git a/table.go b/table.go index 1c06796..0aa8f0d 100644 --- a/table.go +++ b/table.go @@ -3,9 +3,9 @@ package gel import ( "image" "sort" - - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" + + l "gioui.org/layout" + "gioui.org/op" ) type Cell struct { @@ -134,7 +134,7 @@ func (t *Table) Fn(gtx l.Context) l.Dimensions { t.body[i][j].getWidgetDimensions(gtx1) } } - + // find the max of each row and column var table CellGrid table = append(table, t.header) @@ -180,7 +180,7 @@ func (t *Table) Fn(gtx l.Context) l.Dimensions { return dims }) } - + var out CellGrid out = CellGrid{t.header} if t.reverse { diff --git a/testutil_test.go b/testutil_test.go index 0abe5ac..e49435b 100644 --- a/testutil_test.go +++ b/testutil_test.go @@ -5,13 +5,13 @@ import ( "image/color" "testing" - "github.com/p9c/p9/pkg/gel/fonts/p9fonts" - "github.com/p9c/p9/pkg/opts/binary" - "github.com/p9c/p9/pkg/opts/meta" - "github.com/p9c/p9/pkg/qu" + "git.mleku.dev/mleku/prevara/fonts/p9fonts" + "git.mleku.dev/mleku/prevara/pkg/opts/binary" + "git.mleku.dev/mleku/prevara/pkg/opts/meta" + "git.mleku.dev/mleku/prevara/pkg/qu" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" + l "gioui.org/layout" + "gioui.org/op" ) // testWindow creates a Window suitable for testing without actual GUI. diff --git a/text.go b/text.go index be1ae56..3730a10 100644 --- a/text.go +++ b/text.go @@ -3,12 +3,12 @@ package gel import ( "image" - "github.com/p9c/p9/pkg/gel/gio/font" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" + "gioui.org/font" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/text" + "gioui.org/unit" "golang.org/x/image/math/fixed" ) diff --git a/textinput.go b/textinput.go index dfbd3c9..15ad4e6 100644 --- a/textinput.go +++ b/textinput.go @@ -4,14 +4,14 @@ import ( "image" "image/color" - "github.com/p9c/p9/pkg/gel/gio/font" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" + "gioui.org/font" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/paint" + "gioui.org/text" + "gioui.org/unit" - "github.com/p9c/p9/pkg/gel/f32color" + "git.mleku.dev/mleku/prevara/f32color" ) // TextInput is a simple text input widget diff --git a/texttable.go b/texttable.go index 3afd22e..1e7de4e 100644 --- a/texttable.go +++ b/texttable.go @@ -1,6 +1,6 @@ package gel -import l "github.com/p9c/p9/pkg/gel/gio/layout" +import l "gioui.org/layout" type TextTableHeader []string @@ -67,7 +67,7 @@ func (tt *TextTable) Regenerate(fully bool) { // startIndex = len(tt.Table.body) // D.Ln("startIndex", startIndex, len(tt.Body)) // if startIndex < len(tt.Body) { - + // bd := tt.Body // [startIndex:] diff := len(tt.Body) - len(tt.Table.body) // D.Ln(len(tt.Table.body), len(tt.Body), diff) diff --git a/theme.go b/theme.go index 500f588..3c41efb 100644 --- a/theme.go +++ b/theme.go @@ -1,11 +1,11 @@ package gel import ( - "github.com/p9c/p9/pkg/gel/gio/font" - "github.com/p9c/p9/pkg/gel/gio/text" - "github.com/p9c/p9/pkg/gel/gio/unit" - "github.com/p9c/p9/pkg/opts/binary" - "github.com/p9c/p9/pkg/qu" + "gioui.org/font" + "gioui.org/text" + "gioui.org/unit" + "git.mleku.dev/mleku/prevara/pkg/opts/binary" + "git.mleku.dev/mleku/prevara/pkg/qu" ) // Theme contains the styling configuration for gel widgets including colors, diff --git a/toast/example/main.go_ b/toast/example/main.go_ index 2c40ceb..7cd0c95 100644 --- a/toast/example/main.go_ +++ b/toast/example/main.go_ @@ -4,12 +4,12 @@ import ( "log" "os" - "github.com/p9c/p9/pkg/gel/gio/app" - "github.com/p9c/p9/pkg/gel/gio/io/system" - "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + "git.mleku.dev/mleku/prevara/gio/app" + "git.mleku.dev/mleku/prevara/gio/io/system" + "git.mleku.dev/mleku/prevara/gio/layout" + "git.mleku.dev/mleku/prevara/gio/op" + "git.mleku.dev/mleku/prevara/gio/op/paint" + "git.mleku.dev/mleku/prevara/gio/unit" "github.com/p9c/p9/pkg/gui" "github.com/p9c/p9/pkg/gui/fonts/p9fonts" diff --git a/toast/toast.go_ b/toast/toast.go_ index 5a6a430..1cce06f 100644 --- a/toast/toast.go_ +++ b/toast/toast.go_ @@ -4,12 +4,12 @@ import ( "image" "image/color" - "github.com/p9c/p9/pkg/gel/gio/f32" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/op/clip" - "github.com/p9c/p9/pkg/gel/gio/op/paint" - "github.com/p9c/p9/pkg/gel/gio/unit" + "git.mleku.dev/mleku/prevara/gio/f32" + l "git.mleku.dev/mleku/prevara/gio/layout" + "git.mleku.dev/mleku/prevara/gio/op" + "git.mleku.dev/mleku/prevara/gio/op/clip" + "git.mleku.dev/mleku/prevara/gio/op/paint" + "git.mleku.dev/mleku/prevara/gio/unit" icons2 "golang.org/x/exp/shiny/materialdesign/icons" "github.com/p9c/p9/pkg/gui" diff --git a/window.go b/window.go index 567e459..44781d9 100644 --- a/window.go +++ b/window.go @@ -4,21 +4,21 @@ import ( "runtime" "time" - "github.com/p9c/p9/pkg/opts/binary" - "github.com/p9c/p9/pkg/opts/meta" + "git.mleku.dev/mleku/prevara/pkg/opts/binary" + "git.mleku.dev/mleku/prevara/pkg/opts/meta" - "github.com/p9c/p9/pkg/gel/clipboard" - "github.com/p9c/p9/pkg/gel/fonts/p9fonts" + "git.mleku.dev/mleku/prevara/clipboard" + "git.mleku.dev/mleku/prevara/fonts/p9fonts" - "github.com/p9c/p9/pkg/qu" + "git.mleku.dev/mleku/prevara/pkg/qu" uberatomic "go.uber.org/atomic" - "github.com/p9c/p9/pkg/gel/gio/app" - "github.com/p9c/p9/pkg/gel/gio/io/event" - l "github.com/p9c/p9/pkg/gel/gio/layout" - "github.com/p9c/p9/pkg/gel/gio/op" - "github.com/p9c/p9/pkg/gel/gio/unit" + "gioui.org/app" + "gioui.org/io/event" + l "gioui.org/layout" + "gioui.org/op" + "gioui.org/unit" ) type CallbackQueue chan func() error @@ -91,14 +91,14 @@ func (w *Window) Overlay(gtx l.Context) { // NewWindowP9 creates a new window func NewWindowP9(quit chan struct{}) (out *Window) { out = &Window{ - scale: &scaledConfig{1}, - Runner: NewCallbackQueue(32), - Width: uberatomic.NewInt32(0), - Height: uberatomic.NewInt32(0), - ClipboardWriteReqs: make(chan string, 1), - ClipboardReadReqs: make(chan func(string), 32), - clipboardReadReady: qu.Ts(1), - clipboardReadResponse: make(chan string,1), + scale: &scaledConfig{1}, + Runner: NewCallbackQueue(32), + Width: uberatomic.NewInt32(0), + Height: uberatomic.NewInt32(0), + ClipboardWriteReqs: make(chan string, 1), + ClipboardReadReqs: make(chan func(string), 32), + clipboardReadReady: qu.Ts(1), + clipboardReadResponse: make(chan string, 1), } out.Theme = NewTheme( binary.New(meta.Data{}, false, nil), @@ -153,7 +153,7 @@ func (w *Window) Open() (out *Window) { return w } -func (w *Window) Run(frame func(ctx l.Context) l.Dimensions, destroy func(), quit qu.C,) (e error) { +func (w *Window) Run(frame func(ctx l.Context) l.Dimensions, destroy func(), quit qu.C) (e error) { // Start background tasks goroutine go func() { ticker := time.NewTicker(time.Second) diff --git a/wraplist.go b/wraplist.go index 5a22a17..dd1d996 100644 --- a/wraplist.go +++ b/wraplist.go @@ -1,7 +1,7 @@ package gel import ( - l "github.com/p9c/p9/pkg/gel/gio/layout" + l "gioui.org/layout" "golang.org/x/exp/shiny/text" )