302 Commits

Author SHA1 Message Date
qiannian 0bf9626a58 io/input: support direct pointer leave events
Allow platform backends to send pointer.Leave directly.
The router delivers it to entered handlers so hover state is cleared normally.
2026-06-16 13:33:08 +02:00
qiannian 15335a2b37 app: [Windows] restore double-click restore for custom title bars
Keep custom move areas mapped to HTCAPTION even when the window is
maximized so custom title bars preserve the standard Windows behavior
where double-click restores the window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-06-16 10:06:47 +02:00
Qian Nian acf5635575 widget/material: add hover state to window decorations
Decorations buttons already use widget.Clickable and draw press ink, but they don't draw a hover/focus background. This makes custom window decorations appear unresponsive when the pointer is over minimize, maximize, or close buttons.

Register each button's system action over its clickable area and draw the same hovered background used by other material buttons. This also lets platforms query the button action from the material decorations hit area.

Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-14 10:50:56 +02:00
Kevin Yuan caccb608a5 app: [Windows] don't propagate WM_WINDOWPOSCHANGED to DefWindowProc
DefWindowProc handles WM_WINDOWPOSCHANGED by sending WM_SIZE and WM_MOVE
messages, which would lead us to handle resizes twice.

Per MSDN, the WM_SIZE handler is made redundant by handling
WM_WINDOWPOSCHANGED:
https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-windowposchanged

Signed-off-by: Kevin Yuan <farproc@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-05-26 12:50:02 +02:00
Kevin Yuan d52632b475 internal/egl: fix loadEGL caching error on Windows
loadEGL used sync.Once incorrectly: the error returned by
loadDLLs was assigned to a local variable inside loadEGL,
so on the second call Do would not run and the
function would return nil even though the DLLs were never
loaded. This caused a nil pointer dereference when callers
proceeded to use _eglGetDisplay and other uninitialized
function pointers.

Fix by replacing the sync.Once with sync.OnceValue, which
correctly caches the return value of loadDLLs across all
calls.

Signed-off-by: Kevin Yuan <farproc@gmail.com>
2026-05-26 08:07:26 +02:00
Egon Elbre dec57aea1c go.mod: upgrade to github.com/go-text/typesetting@v0.3.4
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-05-18 14:30:25 -04:00
Egon Elbre e2e2c1a046 text: avoid creating two Face instances
This way their cache can be shared.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-05-18 14:30:17 -04:00
Egon Elbre e8c1e1ba11 font/opentype: fix font.Face creation
typesetting introduced a cache field that needs to be
properly initialized. Use constructor to avoid the issue.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-05-18 14:30:12 -04:00
Eugene b1cadbdd76 io/input: do not track scroll events as pointers
Scroll events arrived at pointerQueue.Push and went through pointerOf
+ deliverEnterLeaveEvents + deliverEvent like Move/Press/Release. The
side effect: every scroll created or updated a state.pointers entry,
populated p.entered with whatever handlers sat under the wheel
position, and overwrote state.cursor based on hit-test at the scroll
position.

When the platform layer reports scroll with a different PointerID
than mouse-move events for the same physical mouse — which the
Windows backend does (scrollEvent omits PointerID, defaulting to 0,
while pointerUpdate forwards Windows' assigned ID) — the scroll
spawns a phantom state.pointers entry. Subsequent moves go to the
mouse's "real" entry, so the phantom never receives Leave events,
its entered set never empties, and the cleanup at the end of Push
keeps it alive. pointerQueue.Frame then runs hit-test for it every
frame at the user's last scroll position, threading state.cursor
through it after the live pointer's resolution and clobbering it
with whatever's under the scroll position.

The wheel is positional but isn't a pointer. Treating it as one is
the bug. Hit-test inline at the scroll position to find delivery
targets, dispatch via deliverEvent (which already handles filter
matching, scroll axis clamping, and area-local position), and
return without creating or updating a state.pointers entry.

Add a router-level test that fails without the fix: a Move sets the
cursor over a CursorPointer region, a subsequent Scroll over a
CursorText region, and the test asserts the cursor is still
CursorPointer. Pre-fix the scroll's deliverEnterLeaveEvents
overwrites state.cursor with CursorText.

Signed-off-by: Eugene <eugenebosyakov@gmail.com>
2026-05-18 09:05:32 +02:00
Chris Waldon 451b7d3a74 text: drop obsolete comment about NewWindow
Fixes: https://todo.sr.ht/~eliasnaur/gio/681
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2026-05-06 13:29:01 -04:00
Eugene e49c5b02c7 gesture: refresh PointerID on Press and Enter
Click and Hover both stored the first PointerID they observed in
their internal pid field and only updated it when not currently
hovered/entered. Once the gesture became hovered, any later event
under a different PointerID was effectively ignored: Click.Press
fell through 'c.pid != e.PointerID' and was silently dropped, and
Hover could never reset entered when the matching Leave arrived
under a new ID.

The Windows backend enables EnableMouseInPointer
(app/os_windows.go), under which Windows reassigns the same
physical mouse's PointerID across focus changes, window
leave/re-enter, and similar events. Once a widget had been hovered,
every subsequent press on it failed to register, including
widget.Editor's internal clicker that positions the caret on press.
Multi-line editors silently refused to move the caret on click
after the window had received any focus event.

Always take the latest PointerID on Hover.Enter and Click.Press.
The Press/Release handshake still works because Press now records
the press's own PointerID and Release continues to gate on
'c.pid != e.PointerID' so an unrelated pointer's release can't end
the press tracking.

Signed-off-by: Eugene <eugenebosyakov@gmail.com>
2026-04-30 06:19:20 +02:00
Elias Naur dfe4ff0200 app,io/key: go fmt
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-03-17 17:10:59 +01:00
Lucas Rodrigues 65d86895b8 text: render SVG font as black-white outline
Previously, using SVG fonts will cause Gio to render invisible
"characters".

Now, some fonts (like Noto Sans Emoji) will be rendered
based on "Outline". Gio don't support SVG fonts, but now
it will show the outline ("black-and-white") alternative.

Signed-off-by: Lucas Rodrigues <inkeliz@inkeliz.com>
2026-03-16 08:55:01 -04:00
Lucas Rodrigues c3a6e85f5c app: [js] fixes IME
Fix several issues in the IME implementation

- Typing after an editor regains focus will not add text at the begging of the Editor.
- On Safari iOS, selecting a keyboard suggestion no longer duplicates the text.
- Replacing selected text now occurs at the correct position.

This patch change how "input" event is handled, instead of consider all events the
same.

Signed-off-by: Lucas Rodrigues <inkeliz@inkeliz.com>
2026-03-13 08:42:02 +01:00
Lucas Rodrigues 8c2e45b8f8 app,io: [js] change Shortcut key on macOS/iOS
Previously, the Shortcut key was hardcoded as ModCtrl. That
patches tries to identify the OS and change the key.

Fixes: https://todo.sr.ht/~eliasnaur/gio/624
Signed-off-by: Lucas Rodrigues <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-03-13 08:41:45 +01:00
CoyAce 47ab4c97b2 app: [android] remove redundant ConfigEvent in onStop and onSurfaceDestroyed
Config.Focused represents window interaction focus, which should only
change when the window gains or loses user interaction capability.

The focus state is already correctly handled in:
- onResume → focused = true  (window gains interaction focus)
- onPause  → focused = false (window loses interaction focus)

Therefore, sending additional ConfigEvent in onStop and onSurfaceDestroyed
is redundant because:
- onStop is always preceded by onPause
- onSurfaceDestroyed is a low-level surface event unrelated to focus

This cleanup aligns the Android implementation with the correct semantic
that Config.Focused = window interaction focus, not view focus or
surface state.

Signed-off-by: CoyAce <akeycoy@gmail.com>
2026-03-10 13:01:39 +01:00
CoyAce 760369174d app: [iOS] fix focus event for iOS 13.0+ with backward compatibility
UIScene notifications are the correct way to track window focus on
iOS 13.0+, because the Key Window API is scene-level on iOS 13.0+,
but we need to maintain support for iOS 12 and earlier.

Implementation strategy:
- iOS 13.0+: Use UIScene notifications (DidActivate/WillDeactivate)
- iOS 12 and earlier: Keep existing UIWindow notifications as fallback
- Add runtime version checks to select the appropriate API

Key changes in os_ios.m:
- Add @available(iOS 13.0, *) checks
- Register both notification types with conditional logic
- Ensure proper cleanup of observers when moving between windows

See: https://developer.apple.com/documentation/uikit/uiscene?language=objc

Fixes: https://lists.sr.ht/~eliasnaur/gio/%3CCAMAFT9Uyh_JWrkQQt+AmekJWFBqhZPsP_3ZxC1fUNB+=VGGorw@mail.gmail.com%3E
Signed-off-by: CoyAce <akeycoy@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-03-10 10:08:49 +01:00
CoyAce 92fa23b59b app: [android] replace OnFocusChangedListener with onPause/onResume
References: https://lists.sr.ht/~eliasnaur/gio/%3CCAMAFT9Uyh_JWrkQQt+AmekJWFBqhZPsP_3ZxC1fUNB+=VGGorw@mail.gmail.com%3E
Signed-off-by: CoyAce <akeycoy@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-03-09 16:40:12 +01:00
inkeliz f98baf7f76 app: [js] add IME support
Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2026-03-09 09:07:09 +01:00
Elias Naur a6da4083de test: go fmt
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-03-09 08:54:01 +01:00
Thomas Banks bbb54d5f54 app: enable creation of top most windows
Floating windows are rendered above all other non-floating windows.

Apple Documentation: https://developer.apple.com/documentation/appkit/nswindow/level-swift.struct

Signed-off-by: Thomas Banks <thomas@tombanks.me>
2026-02-19 08:04:13 +01:00
Egon Elbre 8b96643490 widget/material: add LayoutWidgets for adding scrollable widgets
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-02-19 08:01:58 +01:00
Egon Elbre 4ed9695d57 layout: add List.Gap for spacing out items
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-02-19 08:01:55 +01:00
Egon Elbre 9b38545fc2 layout: add Flex.Gap for spacing out items
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-02-19 08:01:50 +01:00
Elias Naur 0d08eaa55c app: remove go1.18 go:build conditional
Our go.mod says 1.24, so a go1.18 conditional is redundant.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-02-18 08:36:57 +01:00
Egon Elbre 9ab8095d1a text: fix length check
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-02-18 08:36:57 +01:00
Egon Elbre 3d6cafa94d all: run go fix
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2026-02-18 08:36:57 +01:00
CoyAce 9966e922f9 widget: fix text selection and selection area rendering issues
1. When selecting multiple lines of text, the rendered selection area does not include the last character of the first line.
2. When selecting a specific line (other than the last line) in multiline text, the last character of that line cannot be selected.

Signed-off-by: CoyAce <AkeyCoy@gmail.com>
2026-01-29 07:19:39 -05:00
runitclean 3af0ebb3a8 text: correct arabic diacritics handling
This commit fixes the association of diacritical marks with the proper script
during text segmentation, as well as fixing the visual position of diacritical
marks. The prior code inverted the Y axis positioning for diacritics, which made
them frequently overlap the glyph they were meant to appear above or below.

Signed-off-by: runitclean <runitclean@disroot.org>
2026-01-15 07:33:20 -05:00
CoyAce 99647591f6 app: [Android] delete redundant dataDirChan
since dataDir() must call after main(), it's redundant to use channel to send path

Signed-off-by: CoyAce <AkeyCoy@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-01-07 13:10:33 +01:00
CoyAce e38f80adc6 app/permission: add microphone permissions
Microphone permissions enables the Android permissions RECORD_AUDIO

Signed-off-by: CoyAce <AkeyCoy@gmail.com>
2026-01-07 12:33:12 +01:00
CoyAce 93419a77bd app: [Android] Send focus lost event when the window is backgrounded
Fixes: https://todo.sr.ht/~eliasnaur/gio/679
Signed-off-by: CoyAce <AkeyCoy@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-30 19:10:58 +01:00
Elias Naur c250d7d562 .builds: fix builds
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-28 10:45:38 +01:00
Elias Naur 6e5bbfe8d4 Revert "gpu: replace f32.Point/Rectangle with image.Point/Rectangle"
This reverts commit 36a2fa37c7.

Reason is that text rendering broke on some Android devices[0].

[0] https://lists.sr.ht/~eliasnaur/gio/%3CPH7PR02MB1003858FA1B27C8A53BE96815DDA1A@PH7PR02MB10038.namprd02.prod.outlook.com%3E

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-18 10:13:20 +01:00
CoyAce 42bc707f7c app: [Android] document DataDir limitations
Document that DataDir is not available before main.

References: https://todo.sr.ht/~eliasnaur/gio/229
Signed-off-by: CoyAce <AkeyCoy@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-18 10:10:04 +01:00
inkeliz 7bcb315ee1 app: add custom scheme support
Now, it's possible to launch one Gio app using a custom URI scheme, such as `gio://some/data`.

This feature is supported on Android, iOS, macOS and Windows, issuing a new transfer.URLEvent,
containing the URL launched. If the program is already open, one transfer.URLEvent will be
sent to the current  app.

Limitations:
On Windows, if the program listen to schemes (compiled with `-schemes`), then just a single
instance of the app can be open. In other words, just a single `myprogram.exe` can
be active.

Security:
Deeplinking have the same level of security of clipboard. Any other software can send such
information and read the content, without any restriction. That should not be used to transfer
sensible data, and can't be fully trusted.

Setup/Compiling:
In order to set the custom scheme, you need to use the new `-schemes` flag in `gogio`, using
as `-schemes gio` will listen to `gio://`.

If you are not using gogio you need to defined some values, which varies for each OS:

macOS/iOS - You need to define the following Properly List:
```
<key>CFBundleURLTypes</key>
<array>
  <dict>
        <key>CFBundleURLSchemes</key>
        <array>
          <string>yourCustomScheme</string>
        </array>
  </dict>
</array>
```

Windows - You need to compiling using -X argument:
```
-ldflags="-X "gioui.org/app.schemesURI=yourCustomScheme" -H=windowsgui"
```

Android - You need to add IntentFilter in GioActivity:
```
<intent-filter>
        <action android:name="android.intent.action.VIEW"></action>
        <category android:name="android.intent.category.DEFAULT"></category>
        <category android:name="android.intent.category.BROWSABLE"></category>
        <data android:scheme="yourCustomScheme"></data>
</intent-filter>
```

That assumes that you still using GioActivity and GioAppDelegate, otherwise more
changes are required.

Events are routed to a new app.Events, which are not linked to a specific window.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-15 22:20:54 +01:00
inkeliz 74671a7f9e app/internal/windows: add SendMessage, FindWindow
Also replace calls to the deprecated StringToUTF16Ptr syscall.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-15 22:20:24 +01:00
inkeliz f48cc2c47f app: [Windows] use the app ID for the window class registry
We're about to send messages between multiple instances of the same
program, and we need something to distinguish Gio programs. Use the
app ID.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-15 22:20:24 +01:00
inkeliz 818061a18a app: remove stale reference to NewWindow in the documentation
Signed-off-by: inkeliz <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-15 22:20:24 +01:00
inkeliz 45963441c1 app: [macOS] run main.main off the main thread when Gio is embedded
When Gio is embedded (such as on Android and iOS), we pretend that the
Go library is the main program by running Go main on the main thread.
To avoid deadlock, `app.Main` returns immediately to relinquish control
of the main thread.

This behaviour is suprising (what if something else runs after `app.Main`?)
and more importantly is not compatible with app global events received
by the main goroutine.

Something had to give, and this change starts a new goroutine for calling
Go's main.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-15 22:20:24 +01:00
Elias Naur be8d9df848 Revert "app: optimize window context locking"
This reverts commit 3e601e73c4 because it
results in a blank window on Android.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
Fixes: https://todo.sr.ht/~eliasnaur/gio/671
2025-10-17 14:47:26 +02:00
Elias Naur 3f4f8ba7c1 app: [macOS] make the app delegate optional
Inspired by the discussion at golang.org/issue/70089, this change makes
our particular NSApplicationDelegate implementation optional.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-09-10 23:04:24 +02:00
Elias Naur 6553915e59 app: [macOS/iOS] simplify running functions in the main thread
Using cgo.Handle allows us to pass a reference to a Go function through
the GCD API for running main thread code, saving a goroutine and a channel.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-09-10 22:55:26 +02:00
Walter Werner SCHNEIDER 4c0e526c0b gpu/clip: fix vertex corner positions
Fixes the 1px overlap of curve quads. Without this patch the rendered quads
were skewed vertically and were 2 pixels shorter in height. The NorthWest
pixels were moved to the SouthWest instead of NorthWest and the SouthEast
pixels were moved to the NorthEast instead of SouthEast.

References: https://todo.sr.ht/~eliasnaur/gio/339
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-08-01 21:15:33 +02:00
Walter Werner SCHNEIDER f45039734f internal/stroke: quickly handle zero length normalization
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-28 21:28:31 +02:00
Walter Werner SCHNEIDER 30f8ac10b7 internal/stroke: fix point normalization for unit length.
This fixes the cases where the unit length matches the hypotenuse.

Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-28 21:28:26 +02:00
Walter Werner SCHNEIDER 176570527d internal/stroke: handle zero-length points
Fixes the edge case where a zero point would be normalized to NaN.

Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-28 21:28:22 +02:00
Walter Werner SCHNEIDER 36a2fa37c7 gpu: replace f32.Point/Rectangle with image.Point/Rectangle
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-07-28 19:49:56 +02:00
Walter Werner SCHNEIDER bbb6d05f09 gpu: respect the offset fraction when clipping
Fixes: https://todo.sr.ht/~eliasnaur/gio/534
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-07-28 19:49:56 +02:00
Walter Werner SCHNEIDER a274f6fe0f app: properly initialize editorState for tests
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-07-16 20:39:19 +02:00
Walter Werner SCHNEIDER 0c145b3815 op/clip: add bounds expansion after move
After moving the pen, the next action should update the bounds for the start position.

References: https://todo.sr.ht/~eliasnaur/gio/451
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-16 09:46:32 +02:00
Walter Werner SCHNEIDER ba82ae46d0 all: avoid collides with builtin min/max functions
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-14 10:14:12 +02:00
Walter Werner SCHNEIDER 4e5a344cc2 f32: replace Affine2D{} with AffineId() for identity transformations
References: https://todo.sr.ht/~eliasnaur/gio/655
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-07-12 15:28:03 +02:00
Walter Werner SCHNEIDER 78b54615cc app: [Wayland] add forth and fifth buttons
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-12 09:43:16 +02:00
Walter Werner SCHNEIDER 31564b98c9 f32: add tests for Affine2D transformations
References: https://todo.sr.ht/~eliasnaur/gio/655
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-12 09:43:08 +02:00
Walter Werner SCHNEIDER 3b1effb7f5 f32: use value receiver for Split method on Affine2D
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-09 13:36:30 +02:00
Walter Werner SCHNEIDER d76b4272aa f32: replace Affine2D{} with AffineId() for identity transformations
Reduces ambiguity by introducing AffineId() for representing identity transformation matrices.

References: https://todo.sr.ht/~eliasnaur/gio/655
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-09 13:35:03 +02:00
Walter Werner SCHNEIDER 3e601e73c4 app: optimize window context locking
Avoids unnecessary eglMakeCurrent and thread locking calls to enhance performance.

Fixes: https://todo.sr.ht/~eliasnaur/gio/658
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-08 20:10:34 +02:00
Walter Werner SCHNEIDER b2b12d6288 widget: remove focus on click behavior from Clickable
This is a breaking change, users that need the old behavior can implement it using the existing API.

Fixes: https://todo.sr.ht/~eliasnaur/gio/511
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-08 14:18:57 +02:00
Elias Naur b2f6707ad1 io/pointer: delete documentation that Cancel events have pointer IDs
Cancel events affect the entire gesture and as such all active pointers.

References: https://todo.sr.ht/~eliasnaur/gio/657
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-07-06 09:26:55 +02:00
Walter Werner SCHNEIDER 420f4c32f4 app: [Wayland] don't recreate EGL surface during resize
Fixes: https://todo.sr.ht/~eliasnaur/gio/656
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-07-01 15:56:50 +02:00
Elias Naur ea979b436d app: [Windows] show the maximize button even when MaxSize is set
According to @kkeybbs, pressing the maximize button on Windows only
resizes the window up to its maximum bounds. That means we can leave the
button available, and only hide it when the window has a fixed size.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-07-01 09:22:38 +02:00
Elias Naur d50ef687b8 app: [macOS] limit full screen window size when MaxSize is set
There are two max window size settings on macOS, `contentMaxSize` and
`maxFullScreenContentSize`. Set the latter to avoid a window being
resized larger than its maximum in full screen mode.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-07-01 09:16:48 +02:00
Walter Werner SCHNEIDER 6ce7ffa4ca internal/stroke: fix normal vector size and direction
The normal vector size and direction depend on the input point and the sign of the unit value provided.

Fixes: https://todo.sr.ht/~eliasnaur/gio/576
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2025-06-26 09:04:57 +02:00
Walter Werner SCHNEIDER c3ce484b5e layout: add Values map to Context
Fixes: https://todo.sr.ht/~eliasnaur/gio/654
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-06-24 13:45:59 +02:00
Miles Alan 8104d527c7 internal/debug: go 1.23.8 compat; use strings.Split not strings.SplitSeq
Currently build fails as go.mod uses go 1.23.8 which doesn't have
strings.SplitSeq.  Note: strings.SplitSeq was introduced in go 1.24.

Signed-off-by: Miles Alan <m@milesalan.com>
2025-05-30 21:13:47 -04:00
Marc 809a6d0dc7 app: [Windows] add minimal support for devices with touch screens
The Windows Pointer API
(https://learn.microsoft.com/en-us/windows/win32/api/_inputmsg/) was used
to allow the detection of events when interacting with touch screens. This
also opens the gates for supporting other types of input devices (e.g. pens
and touchpads). Mouse events are now part of the pointer events (primary
events trigger WM_POINTER<DOWN|UP>, whereas secondary ones vanilla
WM_POINTERUPDATE, and cancellations WM_POINTERCAPTURECHANGED).
A fourth and fifth button (usually found in modern mice) has also been added
for completeness, though their integration in other OS-es shall be the
objective of future patches.

Signed-off-by: Marc <marc.leroy@samantree.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-05-27 19:16:47 +02:00
Marc 0eac4f2c6a io/pointer: add forth and fifth buttons
Signed-off-by: Marc <marc.leroy@samantree.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-05-27 19:16:18 +02:00
5684185+vsariola@users.noreply.github.com 1a17e9ea37 layout: avoid heap escapes in Stack and Flex
Stack.Layout and Flex.Layout caused a lot of heap allocations / escapes. The
reason was that scratch space for dims and call and was inside Stack/FlexChild.
child.call.Add(gtx.Ops) confused the go escape analysis and caused the entired
children slice to escape to the heap, including all widgets in it. This caused a
lot of heap 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.

Signed-off-by: vsariola <5684185+vsariola@users.noreply.github.com>
2025-05-26 21:10:49 +03:00
Elias Naur 0a209f7d39 flake.*: upgrade to nixpkgs 25.05, use nixpkgs android SDK
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-05-24 11:47:43 +02:00
inkeliz 0225334124 layout,gesture: add option to handle vertical scroll on horizontal list
Previously, it was impossible to scroll a Axis == Horizontal using
ordinary mouse-wheel. Now, it's possible to set ScrollAnyAxis == True,
which combine scrolling from any direction. That makes possible
to scroll Horizontal lists with vertical mouse-wheel.

By default this option is false, keeping the old behaviour.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2025-05-16 16:52:45 +02:00
Admin f73287be87 all: clean up code, upgrade to modern Go
Signed-off-by: ddkwork
2025-05-05 19:46:39 +02:00
Admin 86668e8b45 go.*: bump Go to 1.23, upgrade dependencies
Signed-off-by: ddkwork
2025-05-05 19:46:39 +02:00
Elias Naur e18db64991 io/pointer: remove Foremost priority
The only known use-case (nested scrolling) now works without special
treatment of the foremost handler.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-04-24 20:31:33 +02:00
Elias Naur efd31ad621 io/input/pointer: ignore grab commands for tags that don't have the pointer
Without this fix, two gestures that both issue GrabCmd on the same frame
will cancel each other. With the fix, the first will win the grab, and
the other ignored.

References: https://todo.sr.ht/~eliasnaur/gio/647
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-04-24 20:05:11 +02:00
Marcel Juffermans 35ec76e516 app: [Windows] correctly center window on startup
When the window is created position it *before* processing the actions
to perform (which may include system.ActionCenter). Note that the
actions are performed in the callback windowProc().

Fixes: https://todo.sr.ht/~eliasnaur/gio/646
Signed-off-by: Marcel Juffermans <mjuffermans@paradigmone.com.au>
2025-04-24 17:06:56 +02:00
Dave Akers cc6048bc25 app: [Wayland] remove window.seat in favor of wlSeat.pointerFocus
Signed-off-by: Dave Akers <dave@dazoe.net>
2025-04-09 10:09:10 +02:00
Dave Akers 016714a668 app: [Wayland] use correct serial for wl_pointer_set_cursor, take 2
To fix #644 the serial passed to wl_pointer_set_cursor must come from
the pointer enter event. To do this we need a place to keep the serial
so I've added a field to the wlSeat struct to hold on to it.

Ref: https://wayland.app/protocols/wayland#wl_pointer:request:set_cursor
Ref: https://wayland-book.com/seat/pointer.html

Fixes: https://todo.sr.ht/~eliasnaur/gio/644
Signed-off-by: Dave Akers <dave@dazoe.net>
2025-04-03 23:59:44 +02:00
Dave Akers a3117d3823 app: [Wayland] use correct serial for wl_pointer_set_cursor
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-04-02 21:27:53 +02:00
Elias Naur fff2375470 layout,io/input: move disabling events from layout.Context to input.Source
The fix for #605 moved the disabling of event delivery from Source
to Context to enable disabled Contexts to still react to commands
(invalidate, focus etc.). However, that change in turn caused #641 where
the exported Context.Source field would no longer know not to deliver
events.

This change partially reverts the fix for #605 by moving disabledness
back to Source, fixing #641. Disabled Sources are left capable of
executing commands, thus keeping #605 fixed.

Thanks to Chris et al for keeping the use-cases straight enough for me
to come up with this (hopefully final) fix.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
Fixes: https://todo.sr.ht/~eliasnaur/gio/641
References: https://todo.sr.ht/~eliasnaur/gio/605
2025-03-27 11:19:25 +01:00
Elias Naur 72a72a2bc2 app: [macOS] don't discard IME session for consistent snippets
An IME session must be discarded when its text content no longer matches
the underlying text component content. However, the check for matching
was too pessimistic; the IME session would be discarded if the new
snippet from the text component was not equal to the snippet reported to
the IME. This change implements a refined check that only discards a
session if the content of the overlap between the new and old snippets
don't match.

Fixes an IME issue reported by Zhang Zj.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-02-23 19:16:02 +01:00
Chris Waldon 14a9fbcc0d go.*: update typesetting for truncator ordering fix
This commit updates our typesetting dependency to a version that properly
bidi-orders truncator runs. This fixes an issue in which the truncator
symbol could appear on the wrong side of text.

Fixes: https://todo.sr.ht/~eliasnaur/gio/634
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2025-02-21 07:00:52 -05:00
Elias Naur 0d23240556 gpu/internal/rendertest: allow nil pixel check functions
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-02-18 15:18:20 +00:00
Elias Naur 77709d1771 gpu: don't store transformed rectangle paths under the same cache key
Fixes incorrect rendering of multiple transformed instances of a
rectangle.

Fixes: https://todo.sr.ht/~eliasnaur/gio/635
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-02-18 15:14:35 +00:00
kurth4cker 4f720af6f2 app: fix miss included doc comment
License identifier is shown in documentation at[1]. This patch fixes it.

1: https://pkg.go.dev/gioui.org/app#hdr-Permissions

Signed-off-by: kurth4cker <kurth4cker@gmail.com>
2025-02-15 08:49:34 +00:00
Elias Naur af446e8b87 app: [macOS] correct error handling for newMtlContext
Fixes: https://todo.sr.ht/~eliasnaur/gio/632
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-02-10 14:00:12 +01:00
vasijob225 95354d80bd app: [Windows] hide the maximize button when MaxSize is set
Also, disable window frame resizing when MaxSize equals MinSize.

Fixes: https://todo.sr.ht/~eliasnaur/gio/631
Signed-off-by: vasijob225 <vasijob225@protonmail.com>
2025-02-02 14:19:44 +01:00
Elias Naur a5068a1996 all: replace golang.org/x/exp/slices with the standard library package
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-30 13:56:29 +01:00
Chris Waldon 593c5fbf4a text: round y offset of trailing newline
This commit tries to ensure that trailing newlines do not introduce more vertical space below
the text than is occupied by a typical text run within the text.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2025-01-28 10:35:52 -05:00
zjzhang adaace864d text: respect line height when layout the last empty line.
Text Shaper set the last empty line height to ascent+decent
of the paragraph break glyph which causes the last visual empty
line to have a smaller line height. This commit tries to fix it
by setting the line height using the line height from the last line.

References: https://todo.sr.ht/~eliasnaur/gio/629
Signed-off-by: zjzhang <zhangzj33@gmail.com>
2025-01-28 10:17:48 -05:00
Elias Naur f200f0e9a3 app: [Windows] don't draw minimized windows
Fixes: https://todo.sr.ht/~eliasnaur/gio/621
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-14 11:29:45 +01:00
Elias Naur 1ae2b9b8fe app: [macOS] don't draw when minimized
References: https://todo.sr.ht/~eliasnaur/gio/621
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-14 11:29:45 +01:00
Elias Naur fe4bf00c70 app: [macOS] propagate unhandled key shortcuts
By propagation, we restore the system behaviour for shortcuts the program
don't want, for example the system beep.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-14 11:12:34 +01:00
Elias Naur 8107ec2206 app: [macOS] remove support for key bindings
The fix for #616 went to far by attempting to support macOS key bindings
through doCommandBySelector. Issue #625 is a consequence, but more
fundamentally, key bindings does not work with support for key.Release
events.

Remove key binding translation and fix #626, and add a check whether an
IME swallowed a key event, keeping #616 fixed.

While here, replace the KEY_ constants with ASCII codes.

Fixes: https://todo.sr.ht/~eliasnaur/gio/625
References: https://todo.sr.ht/~eliasnaur/gio/616
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-13 23:24:25 +01:00
Chris Waldon d2db4f6875 internal/{egl,gl}: [Windows] restrict graphics DLL sources
In order to avoid DLL preloading attacks, we should be careful about where we
load DLLs from. These packages load graphics DLLs, which may be provided by the
OS, by a graphics vendor, or even by individual applications. As such, we can't
restrict loading them to just system32-provided paths. Instead, we invoke
LoadLibraryEx [0] with the LOAD_LIBRARY_SEARCH_DEFAULT_DIRS path, which will search
system32, application-defined paths, and the path of the primary application
executable. This mode ignores the system %PATH% variable, which dramatically
reduces the attack surface of malicious or unintended DLLs.

Applications may add custom paths to the search list by calling the standard
windows AddDllDirectory function [1] prior to attempting to initialize GL.

Thanks to Mohsen Mirzakhani and Utkarsh Satya Prakash for bringing this to
our attention.

[0] https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexa
[1] https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-adddlldirectory

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2025-01-09 21:57:47 +01:00
Elias Naur 971f86ea7e .builds: work around iOS build failure
Later versions of clang no longer accepts our ancient SDK root. Force it
by supressing a warning.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-09 11:25:27 +01:00
Elias Naur e1fbb189e5 .builds: upgrade FreeBSD builder
It's currently failing, and upgrading it may fix it.

While here, track lates FreeBSD to avoid upgrade toil.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-01-09 10:05:03 +01:00
Chris Waldon a206e5e847 app: [Windows] use NewLazySystemDLL for kernel32.dll
In order to avoid DLL preloading attacks, we should always load our system
dependencies using the helper that only searches the system library path.

Thanks to Mohsen Mirzakhani and Utkarsh Satya Prakash for bringing this to
our attention.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2025-01-09 09:54:15 +01:00
Chris Waldon e025ed1344 widget: ensure editor does not trim trailing whitespace
This commit makes the editor widget suppress the trimming of trailing whitespace
so that the spaces can be selected intuitively.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-12-18 13:28:16 -05:00
Chris Waldon b576252963 text: allow disabling space trimming
This commit adds a shaping parameter that disables the trimming of trailing
whitespace from lines. Text editors and similar use-cases want trailing whitspace
glyphs to be selectable, which means they must occupy space.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-12-18 13:28:16 -05:00
Chris Waldon 1d0d5f0383 widget: update text indexing expectations
The typesetting package has smarter line wrapping now, which is making our
test text require fewer lines to display. We needed to update the expected
data accordingly.

I've also added a feature that takes a screenshot of the rendered output of
one of our most complex cursoring tests. This will make it much easier to
verify its behavior in the future. This feature currently only triggers if
the test case fails.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-12-18 13:28:16 -05:00
Chris Waldon 26cddb00b1 text: use upstream bidi visual order algorithm
We've migrated the processing of bidi run ordering into the upstream typesetting
package, so now we can just consume the already-ordered runs instead of computing
their ordering ourselves.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-12-18 13:28:16 -05:00
Benoit KUGLER 0cbbacc45a [text, font] Bump go-text version to 0.2.1
Signed-off-by: Benoit KUGLER <benoit.kugler@gmail.com>
2024-12-18 13:28:10 -05:00
Elias Naur 1d95c7c6b3 flake.*: upgrade to nixpkgs 24.11
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-12-06 10:23:02 +01:00
Elias Naur 7337c06daf app: [macOS] send keypress events for modifier keys
This change generates keypress and release events for modifier keys in macOS.
Specifically the Control, Alt, Shift and Command keys.

Signed-off-by: Jeff Williams <kanobe@gmail.com>
2024-12-04 18:12:15 +01:00
Elias Naur 94355e5201 internal/vk: remove methods on C types, for Go 1.24 compatibility
Go 1.24 no longer allows methods on C types (golang.org/issue/60725).

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-11-26 10:31:04 +01:00
Elias Naur ea456f42c7 gpu: remove compute renderer
The compute renderer is a failed experiment: a better port of the
Vello vector renderer exists[0] and the upcoming Go 1.24 release
no longer builds the gioui.org/cpu module because of #60725.

Remove it.

[0] https://github.com/dominikh/jello

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-11-26 08:59:36 +00:00
Elias Naur 8daff13af6 app: [windows] ensure no callbacks after DestroyEvent
Setting the callback handler to nil in DestroyEvent should have no effect,
but may help debugging #603.

Also don't call the default window handler for WM_DESTROY since we're
already handling it.

References: https://todo.sr.ht/~eliasnaur/gio/603
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 22:06:57 +02:00
Elias Naur aa158e0c9c app: fix race condition between Window.Invalidate and event loop
References: https://todo.sr.ht/~eliasnaur/gio/603
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 20:38:37 +02:00
Elias Naur 520efdfa75 app: fix typo
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 20:29:21 +02:00
Elias Naur d7a1ec7461 app: [macos] don't relay key events handled by the IME
Widgets such as Editor use certain key events such as the backspace key
to implement text editing. On macOS, such key events are sometimes used
by an input method, and in those cases the key effect would be applied
twice: first by the IME and then the Editor.

Report such key events through the doCommandBySelector callback, which
receives key events not handled by the IME.

References: https://todo.sr.ht/~eliasnaur/gio/616
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 19:39:20 +02:00
Elias Naur d4c5e54375 app: remove dead code
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 19:39:20 +02:00
Elias Naur 44ac50506d app: [android] ensure a new frame is always scheduled when visible and animating
Possible fix to #614.

References: https://todo.sr.ht/~eliasnaur/gio/614
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-25 12:16:54 +02:00
inkeliz 38e4b1c6a4 app: [android] fix compatibility with older Android versions
Previously, setHighRefreshRate requires APIs restricted
to Android 30, or higher.

Tested on Android 6.0.1 (released on 2015).

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2024-10-25 11:44:13 +02:00
Chris Waldon 5d886b4d7f go.*: update typesetting for unifont panic fix
This commit updates our typesetting dependency to avoid a crash when shaping GNU
unifont. Thanks to Jeff Williams for raising the issue on the mailing list.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-10-22 19:05:27 +02:00
Elias Naur c7277581f8 app: [Window] don't report Win32ViewEvent before window configuration
Fixes: https://todo.sr.ht/~eliasnaur/gio/609
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur a5f7e7b2c7 app: [Windows] compute min, max window sizes correctly when un-minimzing
Fixes: https://todo.sr.ht/~eliasnaur/gio/608
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 0781e62b56 app: [Windows] don't ignore Min|MaxSize options
The support for minimum and maximum window sizes were broken by a recent
change. Found while investigating #608.

References: https://todo.sr.ht/~eliasnaur/gio/608
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Larry Clapp 95f63c66b6 text,widget/material: Update doc for Shaper & Theme
Note that you should use different Themes, with different Shapers, for
different top-level windows, and explain why.

Signed-off-by: Larry Clapp <larry@theclapp.org>
2024-10-22 19:05:27 +02:00
Elias Naur 6efcb65c4b Revert "app: [Windows] don't draw after WM_DESTROY destroyed the window"
This reverts commit 635df374952019ff8d274646ea9ce040744daa0f because it
didn't fix #603 after all.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur f6e9f6861d app: close the window before reporting a GPU error
When a GPU error occurs forcing the reporting of a DestroyEvent is not
appropriate, because the backend that controls the underlying window
is not aware of the error and will continue to report events.

Replace the crude DestroyEvent by stashing the error and asking the
window nicely to close. The, report the stashed error in the
otherwise regular DestroyEvent.

Hopefully, this second attempt fixes #603.

Fixes: https://todo.sr.ht/~eliasnaur/gio/603
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 6c6cc157c4 layout,io/input: [API] change Context.Disabled to only disable events
Also unexport Source.Enabled because the nil-ness of its embedded router
is now an implementation detail.

Fixes: https://todo.sr.ht/~eliasnaur/gio/605
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 8cf449034c app: [Windows] remove redundant code
Change f7aa4b5c8 changed the fullscreen implementation to no longer require
the position and size of the fullscreen window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 6722c7960a widget: [API] change Decorations to leave the user in control of window state
As suggested by ~egonelbre, Decorations should not be the source of truth
for the windows state, because external gestures may also change state.

This breaking change removes Decorations.Perform and exposes Maximized
as a bool which is the user's responsibility to set.

Fixes: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 97044e53b5 app: [Windows] track window state changes initiated by the OS
Like the previous change, update the Windows backend to track and report
window state changes initiated by the OS.

References: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur af6dda67a5 app: [macOS] track window state changes initiated by the operating system
Before this change, the window state was explicitly updated whenever
Window.Option was called. However, the system may also change window
state as a result of user gestures, but those changes did not result in
ConfigEvents reflecting them.

Remove the explicit state updates and track them when the system tells
us it has changed.

This is a step towards fixing #600 which require accurate window state
tracking.

References: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 042ed4ab49 app: [Windows] don't draw after WM_DESTROY destroyed the window
There may be a window of time from WM_DESTROY is received to the WM_QUIT
message is delivered by PostQuitMessage. If so, we must not call w.draw.

Fixes: https://todo.sr.ht/~eliasnaur/gio/603
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 6aa027136e app: [Windows] remove redundant code
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur b1db32ef72 io/input: fix typo
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:27 +02:00
Elias Naur 9dceca6c95 Revert "app: [Windows] suppress double-click behaviour for custom decorations"
This reverts commit 82cbb7b8da.

As Egon Elbre points out in

https://todo.sr.ht/~eliasnaur/gio/600#event-377179

this is not the right fix.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-10-22 19:05:19 +02:00
Egon Elbre 7293fa8a41 go.mod: update golang.org/x dependencies
This gets rid of a CVE warning about golang.org/x/image/tiff.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2024-07-16 15:59:05 +02:00
Elias Naur 82cbb7b8da app: [Windows] suppress double-click behaviour for custom decorations
Fixes: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-07-13 18:09:13 +02:00
Chris Waldon 6c19821a6c app: [Wayland] prevent recursive scroll event processing
This commit zeroes the accumulated scroll distance on the window before invoking the
event delivery code, since the event delivery code is able to call back into the scroll
processing. Prior to this change, the callback could re-processing the scroll delta
while magnifying it by a factor of 10.

Fixes: https://todo.sr.ht/~eliasnaur/gio/599
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-07-13 11:03:14 +02:00
Elias Naur 86349775b7 app: ensure Invalidate can be invoked when window is closing
This commit ensures that it is safe to invoke Invalidate() from another goroutine
while a Gio window may be in the process of closing. It can be difficult to prevent
this from happening, as window handles can easily be managed by a type that doesn't
know the exact moment of window close (it might be waiting on the window event loop
to return, but that hasn't happened yet). Without this change, the nil window
driver results in a panic in this situation.

Co-authored-by: Chris Waldon <christopher.waldon.dev@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-07-02 15:06:52 +02:00
Chris Waldon 4a1b4c2642 app: add cross-platform empty view event detection
Custom rendering applications need to be prepared to handle empty view events,
as an empty view event is sent during window shutdown. However, the current
implementation requires applications to write a platform-specific helper
function for each supported platform in order to check whether a received
view event is empty. This commit provides a safe, convenient, cross-platform
method that applications can use to detect this special view event and respond
to it.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-06-28 08:46:36 +02:00
Elias Naur c900d58fb3 app: [macOS] fix ANGLE renderering
Setting CAMetalLayer.presentWithTransaction to YES breaks ANGLE rendering.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 17:48:08 +02:00
Elias Naur 74ccc9c2c7 app: use empty frame when FrameEvent.Frame isn't called
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 16:37:48 +02:00
Elias Naur 3f671afea8 app: ignore Invalidate for Windows not yet created
While here, don't overflow the Windows event queue.

Fixes: https://todo.sr.ht/~eliasnaur/gio/596
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 16:37:48 +02:00
Elias Naur 42357a29e0 app: reset Window when DestroyEvent is received
Fixes: https://todo.sr.ht/~eliasnaur/gio/595
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-27 10:41:22 +02:00
Elias Naur 8fb6d3da2b io/input: deliver all observed events before deferring the rest
Even when a command defers event delivery to the next frame, the already
observed events must still be delivered in the current frame. This
matters for pointer events that hit more than one event handler.

Fixes: https://todo.sr.ht/~eliasnaur/gio/594
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-20 15:26:33 +02:00
Elias Naur 706940ff9b io/input: improve documentation, code
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-20 13:23:36 +02:00
Chris Waldon 5542aac772 app: queue system actions before first call to Event()
This commit ensures that attempting to perform a system window action prior
to the first call to Event() does not panic. It adopts a similar strategy to
handling Option() prior to the first call to Event(): make a slice of the arguments
and apply them during window initialization.

Fixes: https://todo.sr.ht/~eliasnaur/gio/593
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-06-20 12:18:42 +02:00
Elias Naur 026d3f9daa .builds: increase file descriptor limit for Android's sdkmanager
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-20 09:53:17 +02:00
Elias Naur 38fca9ae13 widget: show software keyboard when a writable Editor is clicked
Extracted from https://github.com/gioui/gio/pull/138 by Inkeliz.

References: https://todo.sr.ht/~eliasnaur/gio/591
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-16 15:20:49 +02:00
Elias Naur e878dbc598 app: [macOS] ignore focus changes not meant for the Gio view
Extracted from https://github.com/gioui/gio/pull/138 by inkeliz.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-16 15:15:40 +02:00
inkeliz 1151eac07d pointer: fix documentation
Previously it uses event.Op{}, but such struct don't
exists anymore. Instead, it have a function with the
same name.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2024-06-07 10:35:07 +02:00
Elias Naur 56177c55cf io/input: remove unused field
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-07 10:32:44 +02:00
Elias Naur e6da07a85a app: [iOS] add support for buildmode exe
Up until now, the iOS part has relied on a tool such as gogio to
synthesize a main function. This change adds support for running direcetly
in exe mode, while retaining support for embedded Gio in C programs.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-02 12:55:36 +02:00
Elias Naur 175e134478 app: [macOS] panic if Main is not called from the main goroutine
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-06-02 09:07:29 +02:00
Elias Naur 46cc311d19 app: fix typos
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-30 10:04:36 +02:00
Elias Naur b8821875ed Revert "app: [macOS] ensure the Window is initalized before Run functions"
This reverts commit 5083a23301 because Option
and Run no longer create the window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-30 10:03:37 +02:00
Elias Naur f6e33914d9 Revert "app: [Windows] ensure the Window is initalized before Run functions"
This reverts commit 971b01d836 because Option
and Run no longer creates the window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-30 10:03:37 +02:00
Elias Naur a394b330e8 app: defer window creation until Window.Event is called
We're moving towards making Window.Event, and in the future, Window.Events
create the window and drive the event loop to completion. In that model,
the other Window methods shouldn't create the window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-30 10:03:32 +02:00
Jack Mordaunt 24b0c2a4a1 internal/gl: [Windows] allow GetProgramInfoLog to return nothing
If GetProgrami returns 0 we will panic because a zero-sized buffer indexed
at zero will OOB panic: "runtime error: index out of range [0] with length 0".

This was observed and is not theoretical.

Windows 8 9200

Hardware: {
  "board": {
    "name": "1963",
    "vendor": "Hewlett-Packard"
  },
  "cpu": {
    "name": "Intel(R) Core(TM) i7-4700MQ CPU @ 2.40GHz",
    "vendor": "GenuineIntel",
    "extra": {
      "cores": "4",
      "threads": "8"
    }
  },
  "ram": "8.00GB"
}

panic({0x7f7222ff260?, 0xc004b960d8?})
	runtime/panic.go:770 +0x132
gioui.org/internal/gl.(*Functions).GetProgramInfoLog(0xc004cd4000?, {0x7f720039345?})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/internal/gl/gl_windows.go:365 +0xc5
gioui.org/gpu/internal/opengl.(*Backend).newProgram(0xc004c8e008, {{0x7f7229d0b40, 0xc004e3a000}, {0x7f7229d0b60, 0xc004e3a120}, {{0xc002b04060, 0x2, 0x2}, 0x10}, {0x1, ...}, ...})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/internal/opengl/opengl.go:954 +0x26b
gioui.org/gpu/internal/opengl.(*Backend).NewPipeline(0x2ec?, {{0x7f7229d0b40, 0xc004e3a000}, {0x7f7229d0b60, 0xc004e3a120}, {{0xc002b04060, 0x2, 0x2}, 0x10}, {0x1, ...}, ...})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/internal/opengl/opengl.go:918 +0x65
gioui.org/gpu.createColorPrograms({_, _}, {{0x7f72241fcbb, 0x9}, {0x0, 0x0}, {0x7f72259c680, 0x42a}, {0x0, 0x0}, ...}, ...)
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:601 +0x332
gioui.org/gpu.newBlitter({0x7f7229fea48, 0xc004c8e008})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:559 +0x358
gioui.org/gpu.newRenderer({0x7f7229fea48, 0xc004c8e008})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:516 +0x25
gioui.org/gpu.(*gpu).init(...)
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:373
gioui.org/gpu.newGPU({0x7f7229fea48, 0xc004c8e008})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:365 +0x14d
gioui.org/gpu.NewWithDevice({0x7f7229fea48, 0xc004c8e008})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:355 +0x10c
gioui.org/gpu.New({0x7f7229cbdc0?, 0xc004bd2000?})
	gioui.org@v0.4.2-0.20231216201919-2128f7adea9b/gpu/gpu.go:342 +0x34

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
2024-05-25 18:24:35 +02:00
Walter Werner SCHNEIDER 7a9ce51988 widget: add more editor shortcuts
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2024-05-06 08:46:20 -04:00
Walter Werner SCHNEIDER 8242234274 internal/stroke: fix normal vector size
With this change the GPU renderer now properly handles the cases when the stroke width equals the stroke length where the normal vector is the same size as the original vector.

Fixes: https://todo.sr.ht/~eliasnaur/gio/576
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2024-05-06 10:45:29 +02:00
Elias Naur 691adf4e77 app: [X11] don't recreate EGL surface during resize
According to #565 X11 GPU drivers don't deal well with recreation of
EGL surfaces.

Thanks to Walter Schneider for debugging this issue and coming up with
the original patch.

Fixes: https://todo.sr.ht/~eliasnaur/gio/565
Co-authored-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-05-01 19:59:26 +02:00
Elias Naur ba1e34e570 app: [X11] add missing check for destroyed window
Fixes: https://todo.sr.ht/~eliasnaur/gio/577
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-26 16:44:50 +02:00
owhionetrusetuhecruos@schn.email 0deb7b3efc material: improve progress indicator layout
Fixes: https://todo.sr.ht/~eliasnaur/gio/570
Signed-off-by: Walter Werner SCHNEIDER <contact@schnwalter.eu>
2024-04-21 08:51:20 +02:00
Elias Naur e8c73bcb37 app: [Wayland] suppress spurious ConfigEvents
As reported By Larry Clapp, Wayland would send a ConfigEvent with
every FrameEvent when fallback client side decorations are enabled.
This is because Window would call the driver Option and Perform
methods even when they're empty.

The change applies to every platform, but was only observable on
Wayland.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-20 20:25:08 +02:00
Elias Naur cf9f2bbffe app: [Wayland] don't send events after DestroyEvent
Like a previous commit for X11, this change ensures no events are
sent after DestroyEvent.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-20 20:25:03 +02:00
Elias Naur ed28861309 app: [X11] don't send events after DestroyEvent
Before this change, a FrameEvent may be delivered after DestroyEvent,
leading to a panic. Destroy the X11 window immediately thus ensuring no
events can be delivered after destroy.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-20 09:08:06 +02:00
Elias Naur 971b01d836 app: [Windows] ensure the Window is initalized before Run functions
Like the previous commit for macOS, this defers event processing until
after the Window is ready.

Fixes: https://todo.sr.ht/~eliasnaur/gio/575
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-19 17:19:45 +02:00
Elias Naur 5083a23301 app: [macOS] ensure the Window is initalized before Run functions
Don't call eventLoop.FlushEvents which in turn applies Options and
executes Run functions before the window is fully initialized.

References: https://todo.sr.ht/~eliasnaur/gio/575
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-19 17:08:14 +02:00
Elias Naur 61b603d521 .builds: bump builders to Go 1.22
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-15 10:21:41 +02:00
Egon Elbre 3b5148a64e op/clip: add note about Path.End
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2024-04-15 10:18:25 +02:00
Elias Naur ee6cdec60b io/pointer: [API] split scroll bounds into two separate axes
A single image.Rectangle for the scroll bounds introduced a subtle issue
with zero area rectangles (see #572). To avoid that and similar issues,
split the bounds into two separate one-dimensional ranges.

Fixes: https://todo.sr.ht/~eliasnaur/gio/572
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-14 08:54:12 +02:00
Elias Naur 42ef3476cc go.mod: bump minimum Go to 1.21
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-14 08:44:46 +02:00
Elias Naur 98d3a2eb24 gpu: fix viewport arguments for opacity layers
Fixes: https://todo.sr.ht/~eliasnaur/gio/574
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-12 23:34:18 +02:00
Elias Naur 109226b7e9 gpu: ensure opacity layers are rendered with correct pixel formats
FBOs and window framebuffers generally have different pixel formats, and
so require separate pipeline configurations.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-12 18:08:20 +02:00
Elias Naur 477bd5c744 gpu: remove unused parameter
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-04-12 17:20:23 +02:00
Chris Waldon 1802761c93 go.*: update go-text
This picks up some improvements to face splitting and line wrapping within the
text stack.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-03-29 13:29:03 -04:00
Chris Waldon 0558bb3f1c widget: update test expectations
This commit fixes our tests to expect some whitespace-handling changes in upstream
go-text.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-03-29 13:29:03 -04:00
Benoit KUGLER 78ce5e3ad5 deps: bump go-text/typesetting version to v0.1.0
Using this stable release should ensure user upgrading gio with go get -u do not encouter compilation error

Signed-off-by: Benoit KUGLER <benoit.kugler@gmail.com>
2024-03-29 13:29:03 -04:00
Aman Karmani 1be34eec6f app: [tvOS] fix build failures
Fixes: https://todo.sr.ht/~eliasnaur/gio/567
Signed-off-by: Aman Karmani <aman@tmm1.net>
2024-03-06 21:49:42 +00:00
Elias Naur 44ede4eceb app: update documentation for Window.Run
Window events are no longer asynchronous, so deadlocks are no longer
possible when calling Run.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-03-06 21:36:00 +00:00
Elias Naur 993ec907be app: introduce Config.Focused that tracks the window focus state
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-03-06 20:49:44 +00:00
Elias Naur 35785e9c96 app: [macOS] synchronize rendering with Core Animation for smooth resizes
Magic incantations lifted from

https://thume.ca/2019/06/19/glitchless-metal-window-resizing/

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-03-06 20:49:44 +00:00
Elias Naur 93ac0b03f1 app: [API] rename Window.NextEvent to Event to match Source.Event
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:52:04 +00:00
Elias Naur d58d386b9b app: [API] remove StageEvent and Stage
StageEvent served only redundant purposes:

- To detect whether the window has focus. That is covered by
  key.FocusEvent.
- To detect whether the window is currently visible. That is covered by
  the absence or presence of FrameEvents.
- To detect when the window native handle is valid. That is
  covered by ViewEvent.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:52:04 +00:00
Elias Naur f3fc0d62b8 app: [API] make ViewEvent an interface on all platforms
A uniform type allows convenient nil checks and for future window
backends on platforms other than Linux (which already had ViewEvent
as an interface).

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:51:38 +00:00
Elias Naur 5e5d164929 app: [macOS] move destruction to NSView.dealloc
The dealloc method is where we're guaranteed the NSView is no longer
used anywhere.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:26 +00:00
Elias Naur 1527e91a02 app: [macOS] send ViewEvents when the NSView is attached to a NSWindow
Instead of sending ViewEvents once at construction and once at destruction,
it's better to send them when the underlying NSView changes attachment.

The main advantage is that we're about to move the destruction and
emitting of DestroyEvent to the NSView's dealloc method. However, the
dealloc will not be called if user code has a strong reference to it
through a non-empty ViewEvent. By sending an empty ViewEvent when the
view is detached, well-behaving users will remove the strong reference.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:26 +00:00
Elias Naur caba422d9c app: [macOS] make gio_trySetPrivateCursor static, remove its prefix
While here, don't use trySetPrivateCursor for the public openHandCursor
and closedHandCursor.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:26 +00:00
Elias Naur 390242f214 app: [macOS] add missing autoreleasepools
Their absense didn't make a practical difference so far, but we're about
to refactor the macOS event processing loop where the pools do matter.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:26 +00:00
Elias Naur fe1df00d02 app: merge with internal log package to remove the separate log.appID
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:26 +00:00
Elias Naur 0d7f00c634 app: [macOS] use cgo.Handle for referring to Go windows from native code
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:26 +00:00
Elias Naur d7528a8338 app: [macOS] use NSNotificationCenter to receive app events
Notifications don't require a list of windows nor an app delegate.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:26 +00:00
Elias Naur 9bca5bfdcf app: [iOS] use cgo.Handle for referring to Go windows from native code
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:26 +00:00
Elias Naur a880d6403d app: [API] make the zero-value Window useful and delete NewWindow
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:26 +00:00
Elias Naur 6879a30582 app: prepare Window for removal of Main and asynchronous FrameEvents
This is mostly a refactor, but there are two user-visible effects:
- Window.NextEvent may be called even after DestroyEvent is returned.
- Window.Invalidate always wakes up a blocking NextEvent, even when a
FrameEvent cannot be generated.

As a nice side-effect, X11, Wayland and Wasm no longer require separate
goroutines for their window loops.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:45:15 +00:00
Elias Naur 5cda660e6e app: slim down window.go by moving editorState to separate file
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 17:21:59 +00:00
Elias Naur 8cb06ffa30 app: [Wayland] fix reference to most recent metric
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 17:21:59 +00:00
Chris Waldon 297c03925d widget: [API] simplify Selectable event processing
Now (*widget.Selectable).Update() returns whether the selection changed during
event processing, rather than requiring a separate call to (*widget.Selectable).Events().

The Events() method has been removed as redundant.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-02-05 11:09:36 +00:00
Chris Waldon c645c2ec8e widget: [API] convert Editor to return one event at a time
This commit eliminates (*widget.Editor).Events() in favor of making
(*widget.Editor).Update() return events as they are generated in response to
input. This makes the behavior of the editor match the rest of the core widgets.
Callers who previously invoked Events() can now achieve the same thing by using
a loop like this:

for {
	ev, ok := editor.Update(gtx)
	if !ok {
		break
    }
	// Handle ev
}

This is undeniably more verbose, but it enables more sophisticated event processing.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-02-05 11:09:36 +00:00
Chris Waldon 95ca7b5b59 io/input: fix docs for Router.Queue
The method no longer returns anything, and thus does not actually report
whether any events matched a handler.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-02-05 11:09:36 +00:00
Elias Naur 5a843bee61 widget: update documentation
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur dbc10056f9 io/event: [API] rename InputOp to Op
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur eae39d8556 app: update documentation
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur e59f91dfd0 io/input,widget: [API] replace per-widget Focused with Source.Focused
Widgets have themselves as tags, by convention, and so it's possible to
replace the per-widget Focused methods with a general-purpose Source.
Focused query.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur c3f2abebca io/input: implement key.Filter.Name special case for matching every key
The empty key.Filter.Name now means matching every key name. This is a
replacement for the previous special case where the top-level key.InputOp
handler would get all unmatched events.

Add special case for system events such as focus switch shortcuts.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 77ff21605c io/input: test Router.TextInputHint
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur f5aa745038 io/input: discard pointer reset event if filter doesn't match
New handlers receive reset events the first time Source.Event is called.
However, in case the filter doesn't match a reset event it shouldn't be
reported.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 1fc646a8c2 io/input: test deferred behaviour of Router
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 33f9a850c8 io/pointer: make Cancel non-zero
It's semantically problematic that a zero Kind matches Cancel, and
outweighs the downside of having to explicitly mention Cancel in filters.
For example, GrabCmd was always deferred because the resulting Cancel
events always match the processed filters.

Remove Frame from a few tests now that GrabCmd can be executed
immediately.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 5fcfc40ab8 text,widget: remove dead code and fields
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 20c28ef282 io/input: tighten tests
Now that event delivery can be interleaved with commands, tests can be
made more precise.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur ed0d5d5767 all: [API] deliver key events to the first matching filter
Replace the key.Filter.Target field with a Focus field that matches only
of the specified tag has focus. This has the advantage of simpler event
delivery and for lower latency in delivering key events to new handlers.

For example, consider a UI where a button is activated by a key press,
which is turn displays a dialog with another button activated by the
same key. This change allows two button press(+releases) in the same frame
to arrive at the intended targets: one key press(+release) for each
button.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur d9a007586c all: [API] replace tag parameter of Source.Event with per-filter tags
Until now, every event has had a particular target. We're about to simplify
key event delivery to match the first matching filter, so there is no
longer a global meaning to the tag argument to Source.Event.

Add fields to filters to specify their target tags.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 496fc3cc82 io/input: permit FocusCmd to explicitly set the focus to any tag
If the client asks for the focus to be set to a tag, allow it. There is a
check at the end of Router.Frame that clears the focus if the tag turns
out to fail the requirements (visible and has asked for FocusEvents).

The change simplifies the logic for determining whether a command can
be executed immediately.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 8e209fd2eb gesture: report one event at a time
Events are now delivered one at a time, and this change makes the
corresponding change to gestures.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur ab9f42c820 widget: [API] replace Focus methods with explicit FocusCmds
Now that widgets by convention may be focused by issuing FocusCmd
directly, remove the now redundant Focus methods on Clickable, Editor,
Selectable.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 6dcebf205f widget: show soft keyboard on focus
We're about to replace the per-widget Focus methods with the client
executing FocusCmd themselves. To ensure the soft keyboard is not
forgotten, ask to show it automatically on focus.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 75314fcee2 all: use a single tag per widget for event handling
With the introduction of filters, it is now possible to have one tag per
widget by convention. Note that gestures still have their own tags, for
disambiguation.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur c515b7804e all: replace InvalidateOp with InvalidateCmd command
Curiously, InvalidateCmd is probably the only command that is appropriate
to call during layout.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 0fab08bd6c widget: [API] change Clickable.Update to report one click at a time
Similar to how events are processed one at a time, change Clickable to
report clicks one at a time.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 88f5ac9cb9 all: [API] deliver events one at a time to allow fine-grained event processing
Processing one event at a time allows a widget to execute commands after
the event that triggered it, instead of after all matching events.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur bce1dbd654 io/input: switch internal API to return one event at a time
Make the internal changes to support fine-grained event delivery.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur fc208248b7 io/input: [API] execute commands immediately
Change the semantics of commands to execute immediately. In cases where
execution of a command introduces a inconsistency, freeze event routing
and defer the command as well as queued events to the next frame.

Rename Source.Queue to Source.Execute to better fit the new command
semantics.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 67b58a6006 io/input: merge pointer and key filters
Refactor the pointer and key filter unions into the handler state struct.
This is a preparation for replacing calls to filtersMatches with queries
to the filter union.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 4d8caba6c9 io/input: merge per-handler state
We're about to need per-handler state related to neither pointer nor
key input. This change merges the pointer and key handler state into one
state struct, tracked in the Router.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 9dfada745c io/input: implement lazy event routing
This change defers event routing from the time the event is queued until
the time Events is called. This allows a future change to execute
commands immediately and to react to event order changes during a frame.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 651094d692 io/input: merge event queues
Replace the per-event event queues with a single queue of events, each
marked with the target tag. This change is a prerequisite for lazy event
delivery.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 3ba5fc557c io/input: remove pointerQueue.scratch optimization
We're about the refactor this quite subtle code, and the optimization
doesn't seem to carry its weight in complexity.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur d25912678c io/input: deliver reset events lazily
Refactor delivery of reset events to be resolved and delivered as part of
Source.Events. This is a preparation for changing event handling to be
lazy.

Reset events are delivered to event handlers that are either new or
haven't been active in the previous frame for a particular event type
(pointer or key events), to ensure the handler state is reset.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:36 +00:00
Elias Naur 27ef6dd7a2 io/key: [API] replace key.InputOp with a filter
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 11:09:33 +00:00
Elias Naur 73c3849da4 io/key: [API] introduce FocusFilter for matching focus and editor events
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 12a0ad7038 io/key: [API] add InputHintOp for specifying the input hint for a tag
We're about to replace key.InputOp with a filter; this change separates
the input hint into its own operation.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur ef8171b971 io: [API] introduce event filters; convert pointer input to use them
Instead of having to supply the predicates for event filtering at the
time of layout, the new Filter type allows widgets to filter at the time
of calling Source.Events. There is then only the need for a single input
op type, in package event.

Filters most importantly allow the use of one tag for several event types,
and we can define that a widget w has &w as its primary tag, by convention.
This allows the replacement of per-widget Focus methods with direct uses
of FocusCmd{&w}, and the later addition of Source.Focused(&w) queries.

Note that the TestCursor test needed restructuring to avoid its use of
InputOps.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur d2085ab7c5 io/system,app: [API] move DestroyEvent, StageEvent, Stage to package app
They're only useful at the top-level event loop in combination with an
app.Window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur d7636ea273 widget: remove test dependency on package app
Without the dependency, tests builds much faster.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur be86450ea5 widget/material: drop test dependency on package app
Without the dependency, tests builds much faster.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 1bcbaa8137 io/input,io/pointer: [API] make pointer grab a command
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 676b670119 io/input,io/clipboard: [API] replace ReadOp with command
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur d51aea553f io/input,io/clipboard: [API] replace WriteOp with command
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur a3c539b3c2 io/input,io/transfer: [API] replace OfferOp with command
Also delete two tests that are no longer relevant.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur eed93aaffe io/input,io/key: [API] replace SnippetOp with command
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 813d836641 io/input,io/key: [API] replace SelectionOp with command
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 627e028d3c widget: [API] re-implement Clickable.Focus with a command
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 9de80749e1 widget: [API] re-implement Selectable.Focus in terms of commands
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 8334d2abb4 widget: [API] re-implement Editor.Focus in terms of commands
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 5dd41f74d3 io/input,io/key: [API] replace SoftKeyboardOp with a command
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur be36fc88aa io/input,io/key: [API] introduce Command, replace FocusOp with FocusCmd
Modeling focus change as an operation is awkward, because focus changes
logically happen during event processing, not layout. In particular, you
want to apply focus changes even if a widget is subsequently never laid
out.

Now that input.Source is concrete, it's much more straightforward to
offer focus changes as a command which can be queued through the
Source. A future change may similarly offer a command for directional
focus changes.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur a11f35fe0d io/key,io/input: [API] move FocusDirection to package io/key
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 6027517949 io/input: [API] introduce Source, the interface between a Router and widgets
This change gets rid of the event.Queue interface by replacing it with
input.Source values. Source provides the interface to Router necessary
to implement interface widgets.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur c319f3c214 io/input: remove dependency on package gesture
This change is required to to replace event.Queue with a concrete
input.Source.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 4fcd96ac4b layout,app: [API] rename FrameEvent.Queue and Context.Queue to Source
We're about to replace the interface Queue with a concrete input.Source.
This change renames the field accordingly.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur d5a0d2cf60 io/input,io/router: [API] rename package io/router to io/input
The input name better matches its purpose, in particular when we
introduce input.Source.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 99399184ac widget: remove assumption that Context.Queue is an interface
We're about to make Context.Queue a concrete type, and this change
replaces code that relies on Queue being an interface.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur dd36ec5e07 app: [API] remove assumption that FrameEvent.Queue is an interface
We're about to make the Queue field of FrameEvent (and layout.Context)
a concrete type. Remove the interface assumption from app.Window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur cb1e605203 app,io/system,layout: [API] move FrameEvent and Insets to package app
In the early days of Gio, FrameEvent was part of package app. It was
moved to package system to enable layout.NewContext be a convenient
short-hand for constructing a layout.

However, it seems the better design to leave FrameEvent (and Insets) in
package app, and move layout.NewContext there as well. More importantly,
the move allows us to replace the event.Queue interface with a concrete
type.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 60bfb9e064 io/router: [API] make SemanticID a uint, not uint64
4 billion semantic IDs should be enough for everyone.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur 3648bdc02a io/profile: [API] delete package
It was a design mistake to make profiling data available to programs.
Rather, profiling should either be a user-configurable debug overlay,
reported through runtime/trace, or both.

This change drops the io/profile package because we're about to overhaul
event routing.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
Elias Naur e19a248815 io/key: delete Event.String
The String method doesn't add anything in addition to the default Go
formatting of the type.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-05 10:59:51 +00:00
James Stanley 7cfd226b57 material: fix documentation of using buttons
Signed-off-by: James Stanley <james@incoherency.co.uk>
2024-02-05 10:59:23 +00:00
Danny Wilkins 05d28ad76a internal/gl: fix startup crash on openbsd from libGLESv2 naming
Signed-off-by: Danny Wilkins <tekk.tonic@aol.com>
2024-01-26 15:40:13 -05:00
James Stanley 40706d3782 material: fix documentation of creating an icon
Signed-off-by: James Stanley <james@incoherency.co.uk>
2024-01-16 15:14:46 +00:00
James Stanley adba14c062 material: fix documentation of changing theme colours
Signed-off-by: James Stanley <james@incoherency.co.uk>
2024-01-16 15:14:46 +00:00
Chris Waldon ab021c4566 app: fix automatic window decoration action processing
This commit adapts the use of the automatic window decorations to the
event processing changes introduced in v0.4.0. You must update widget
state before laying it out, not after. Doing so after (as this code used
to do) results in discarding updates.

Fixes: https://todo.sr.ht/~eliasnaur/gio/542
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-01-07 13:34:44 -05:00
Dominik Honnef fe2a164d30 gpu: rename resourceCache to textureCache and use concrete key
The only remaining use of the cache is mapping handles to textures.
Using a concrete type for the key avoids the allocation caused by convT.

If we need more caches again in the future we can copy the type, or make
it generic.

Instead of updating the benchmark, we removed it outright. It suffered
from several flaws:

- The amount of work for each iteration of b.N wasn't constant, because
  the same cache was reused, growing ever larger in size.

- It only tested the cost of insertions. The comment "half are the same
  and half updated" wasn't true, as calling 'put' with the same key twice
  would've resulted in a panic.

- It didn't simulate any particular workload or cache size, making the
  benchmark useless for comparing different cache implementations. The
  cost of insertions isn't particularly interesting.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2024-01-04 11:57:06 -06:00
Dominik Honnef 4eca2c7d26 gpu: remove unused cache parameters
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2024-01-04 11:57:02 -06:00
Dominik Honnef 7ea432fa13 widget: don't refer to non-existent method Clickable.Clicks
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2024-01-04 11:56:56 -06:00
Dominik Honnef e666ef35ca gesture: adjust ClickKind.String for ClickType -> ClickKind rename
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2024-01-04 11:56:50 -06:00
sewn a8ec3968d9 widget/material: allow changing height & radius of progressbar
Signed-off-by: sewn <sewn@disroot.org>
2023-12-19 11:32:43 -06:00
Elias Naur 2128f7adea app: [Windows] tolerate gpu.ErrDeviceLost from Refresh
Fixes: https://todo.sr.ht/~eliasnaur/gio/552
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-12-16 14:19:19 -06:00
Elias Naur a454d5fa38 flake.nix: upgrade to nixpkgs 23.11; upgrade to JDK 17
Apparently, newer Android SDKs now support Java versions newer than 8.
Finally.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-12-16 14:19:09 -06:00
Elias Naur 7d1ea02267 app: don't route internal wakeup events to the Router
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-12-16 14:18:22 -06:00
Elias Naur f7aa4b5c81 app: [Windows] fix restore size when leaving fullscreen
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-12-16 14:04:19 -06:00
Chris Waldon 52987e53f6 widget/material: fix list scrollbar display
This commit fixes a visual misalignment in scrollbars resulting from subtle differences
in the semantics of layout.Stack and layout.Background. layout.Stack will position expanded
children according to their minimum constraint regardless of their returned size, whereas
layout.Background uses their returned size. This means that layout.Expanded widgets returning
zero dimensions are positioned correctly, but they break when converted to use layout.Background.

This commit fixes the problem by returning correct dimensions from the scrollbar track.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-12-08 11:27:00 -05:00
Chris Waldon e32417353a widget: [API] rename scrollbar update method to update
This matches the convention of other state update methods. While here, remove useless
dimensions return. The update doesn't draw anything, so there are no dimensions involved.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-12-08 11:27:00 -05:00
Elias Naur c458eb30f0 widget/material: add missing Update calls
Without the updates, the switch and radiobutton would use stale state
for layout.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-12-05 10:40:45 -06:00
Chris Waldon d96c954769 io/pointer: fix godoc reference to renamed type
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-11-30 08:58:40 -05:00
Egon Elbre f39245df99 layout: add Background
It's relatively common to create a widget and then add a background to
it. Using layout.Stack causes bunch of heap allocs, which we would like
to avoid whenever we can.

This adds layout.Background which is roughly the same as:

    layout.Stack{Alignment: layout.C}.Layout(gtx,
    	layout.Expanded(background),
    	layout.Stacked(widget)
    )

goos: windows
goarch: amd64
pkg: gioui.org/layout
cpu: AMD Ryzen Threadripper 2950X 16-Core Processor
     │    Stack     │             Background              │
     │    sec/op    │   sec/op     vs base                │
*-32   203.80n ± 1%   83.36n ± 3%  -59.09% (p=0.000 n=10)

     │   Stack    │             Background             │
     │    B/op    │   B/op     vs base                 │
*-32   48.00 ± 0%   0.00 ± 0%  -100.00% (p=0.000 n=10)

     │   Stack    │             Background              │
     │ allocs/op  │ allocs/op   vs base                 │
*-32   2.000 ± 0%   0.000 ± 0%  -100.00% (p=0.000 n=10)

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-11-25 11:50:25 -06:00
Siva 8097df9930 app: [macOS] activate app on ActionRaise if necessary
Calling window.Perform(system.ActionRaise) does not show the window on
the top if the app is currently not active. This can happen for example
if the app integrated with systray (https://pkg.go.dev/fyne.io/systray)
where the menu item launches a window, the window is not showing at the
top. It is fixed by activating the current app if necessary.

Signed-off-by: Siva Dirisala <siva.dirisala@gmail.com>
2023-11-21 10:40:57 -06:00
Elias Naur fc6e51deba .builds: fix apple builder
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-11-15 10:53:59 -06:00
Egon Elbre 5fa94ff67b op/paint: add nearest neighbor scaling
This adds support for nearest neighbor filtering,
which can be useful in quite a few scenarios.

  img := paint.NewImageOp(m)
  img.Filter = paint.FilterNearest
  img.Add(gtx.Ops)

Fixes: https://todo.sr.ht/~eliasnaur/gio/414
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-11-15 10:38:02 -06:00
Elias Naur 23b6f06e3e io/pointer: clarify the documentation for Event.Position
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-11-15 08:07:12 -06:00
Chris Waldon c8801fe233 widget: test update-only editor logic
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-11-10 16:17:56 -05:00
Chris Waldon 3fde0c0061 widget: [API] split text widget Update from Layout
This commit introduces Update(gtx) functions for both Selectable and Editor, allowing their
state to be updated explicitly prior to layout. This completes the transition that allows all
Gio widgets to have their state updated ahead-of-time, ensuring that there is zero frame lag
between an input event and the widget response to that event.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-11-10 14:59:06 -05:00
Chris Waldon 9d89f7c8b1 text: add system font loads to debug log
This commit adds a GIODEBUG=text log message each time a system font is resolved.
This makes it vastly easier for application authors to determine which system fonts
are being used by their application.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-11-10 08:27:43 -05:00
Egon Elbre 48bd5952b1 widget: optimize processGlyph
processGlyph does not modify the value, so there's no reason to
return the struct.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-11-09 15:18:46 -05:00
Egon Elbre df8a8789a3 text: [API] reduce size of Glyph.Runes to uint16
This shrinks text.Glyph from 72B to 58B.

  LabelStatic/1000runes-RTL-arabic-32   63.62µ ± 0%   62.05µ ± 0%  -2.47% (p=0.002 n=6)

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-11-09 15:18:46 -05:00
Egon Elbre 62edabe137 text: use a simpler hash
The hash calculation is a significant bottleneck in caching,
replace it with a simpler "add; multiply by a prime" approach.

  LabelStatic/1000runes-RTL-arabic-32    89.75µ ± 2%   63.58µ ± 1%  -29.16% (p=0.002 n=6)

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-11-09 15:18:46 -05:00
Egon Elbre 49296bd0ca internal/ops: use uint32 for pc, version, macroID
4GB of render data should be sufficient for anyone.

By replacing an int with uint32, it allows for a smaller memory
footprint and faster caching. Example impact on rendering static
labels.

                                     │  before.txt  │             after.txt              │
                                     │    sec/op    │   sec/op     vs base               │
LabelStatic/1000runes-RTL-arabic-32     98.08µ ± 3%   88.17µ ± 1%  -10.10% (p=0.002 n=6)
LabelStatic/1000runes-RTL-complex-32    103.9µ ± 2%   101.9µ ± 1%   -1.84% (p=0.009 n=6)
LabelStatic/1000runes-RTL-emoji-32      113.3µ ± 2%   100.7µ ± 3%  -11.11% (p=0.002 n=6)
LabelStatic/1000runes-RTL-latin-32     100.01µ ± 1%   92.31µ ± 1%   -7.69% (p=0.002 n=6)
LabelStatic/1000runes-LTR-arabic-32     97.90µ ± 2%   87.92µ ± 2%  -10.19% (p=0.002 n=6)
LabelStatic/1000runes-LTR-complex-32   102.63µ ± 2%   99.81µ ± 1%   -2.75% (p=0.002 n=6)
LabelStatic/1000runes-LTR-emoji-32     106.56µ ± 2%   98.47µ ± 1%   -7.59% (p=0.002 n=6)
LabelStatic/1000runes-LTR-latin-32      97.51µ ± 1%   92.60µ ± 3%   -5.03% (p=0.002 n=6)
geomean                                 102.4µ        95.09µ        -7.10%

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-11-09 15:18:46 -05:00
Elias Naur d078bf0ed7 app: unexport NewDisplayLink
It's not supposed to be used outside of package app.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-31 17:56:24 -05:00
Elias Naur ea58aacde2 app: [macOS] don't free nil string in ReadClipboard
Fixes: https://todo.sr.ht/~eliasnaur/gio/539
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-16 08:24:09 -05:00
Larry Clapp ae2b1f42b2 widget: Update Selectable key filter
Selectable was using a key event filter copied directly from editor.go,
but it didn't actually process all those keys. Update the filter to only
ask for the keys that Selectable actually uses.

Signed-off-by: Larry Clapp <larry@theclapp.org>
2023-10-15 10:44:39 -04:00
Elias Naur 63fea3d2bd widget: use local random source to avoid deprecated rand.Seed
This change replace the global rand use with a local source, to avoid
the recently deprecated global rand.Seed function. At the same time, the
time-dependent seeds are replaced with static numbers to ensure
reproducible benchmarks numbers.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-14 15:45:58 -05:00
Elias Naur ce8475a0b9 Revert "app: [Wayland] avoid a race on the send side of the wakeup pipe"
This reverts commit 7fde80e805, because
Wakeup can no longer be called after the window has been destroyed.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-14 10:02:31 -05:00
Elias Naur 37717d0df9 app: [API] replace events channel with an iterator interface
The goroutine started by Window.run runs concurrently with the user
goroutine receiving from Window.Events, leading to races such as #543.
This change replaces the Window.run goroutine and the Window.Events
channel with an iterator API driven by the user goroutine directly.

Fixes: https://todo.sr.ht/~eliasnaur/gio/543
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-14 10:02:28 -05:00
Elias Naur 7550d85447 .builds: remove unused Chrome
Chrome was required when gogio was part of the repository. It is no
longer.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-08 18:46:52 -05:00
Elias Naur c756986d9e gesture: [API] rename gesture state update methods to Update
Change the gesture state update methods to align with the convention.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-08 12:37:12 -05:00
Elias Naur dc170033cd io/router: [API] drop extra frame
This change removes the extra frame scheduled when events was delivered
during a frame. This extra frame was intended to paper over state changes
that happen later than the layout depending on it.

However, it is better for programs to never allow such state change skew,
and recent changes allows them to refresh and query state before layout.

This is an API change because programs may rely on the extra frames.
Those programs should ensure that state is updated before relying on it
in layout.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 20:04:32 -05:00
Elias Naur d42dae73f0 widget: [API] separate Float state update; remove min, max, invert parameters
This change allows users of Float to determine its state before Layout
by calling Update.

While here, remove the value transformation represented by the min, max,
invert parameters; they're too many arguments for a computation that
may as well be done by the user.

Remove Float.Pos; it is better to compute its value from the dimensions
returned by Float.Layout.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 20:04:32 -05:00
Elias Naur 23e44292bb widget: [API] separate state changes from Draggable.Layout to Update
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 20:04:32 -05:00
Elias Naur fe85136f99 widget: [API] move Enum state update to Changed, rename it to Update
Similar to an earlier change for other widgets, this change separate
Enum state changes for access earlier than Layout.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 20:04:32 -05:00
Elias Naur b9837def5c widget: [API] move Decorations state update to Actions
Similar to a previous change for Clickable and Bool this change separates
state changes from Decorations.Layout to Actions so that access may
happen before Layout.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 20:04:13 -05:00
Elias Naur dc97871122 widget: [API] rename Bool.Changed to Update and move state update to it
Similar to a previous change for Clickable, this change separates Bool
state changes to its renamed method Update. This allows access to
the most recent state before calling Layout.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 20:03:25 -05:00
Elias Naur 4a4fe5a69b widget: [API] move Clickable state update from Layout to Clicks
Before this change, Clickable state updates would happen in Layout.
However, that is too late in cases where clicks affects layout that
contiains the Clickable.

This change removes state changes from Layout and moves them to Clicks,
to allow users pre-layout access. Note that Layout itself processes
events, which means users can no longer access clicks after Layout.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 20:03:22 -05:00
Elias Naur 1686874d07 gesture: [API] rename ClickType to ClickKind
"Kind" is the Go idiomatic name for distinguishing structs outside of
the type system.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 19:11:11 -05:00
Elias Naur 650ccea28d io/pointer: [API] rename PointerEvent.Type to Kind
Kind is the idiomatic field name for distinguishing a struct without
using separate types.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 19:11:08 -05:00
Elias Naur e1b3928819 io/semantic: [API] replace DisabledOp with EnabledOp
The double-negative DisabledOp is harder to understand than a
straightforward EnabledOp. Note that the absence of an EnabledOp
implies still means that the widget is enabled.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-06 18:08:52 -05:00
192 changed files with 11343 additions and 10756 deletions
+19 -9
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Unlicense OR MIT
image: debian/testing
image: debian/stable
packages:
- clang
- cmake
@@ -8,23 +8,28 @@ packages:
- libxml2-dev
- libssl-dev
- libz-dev
- llvm-dev # for cctools
- uuid-dev ## for cctools
- 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.git
- https://github.com/tpoechtrager/apple-libtapi.git
- https://github.com/mackyle/xar.git
- 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.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
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
@@ -42,6 +47,11 @@ tasks:
- 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
@@ -53,7 +63,7 @@ tasks:
./install.sh
- build_cctools: |
cd cctools-port/cctools
./configure --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --target=x86_64-apple-darwin19
./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
@@ -61,4 +71,4 @@ tasks:
CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./...
- test_ios: |
cd gio
CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-ios GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags ios ./...
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 ./...
+2 -2
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Unlicense OR MIT
image: freebsd/13.x
image: freebsd/latest
packages:
- libX11
- libxkbcommon
@@ -16,7 +16,7 @@ environment:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.19.11.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
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 ./...
+11 -8
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Unlicense OR MIT
image: debian/testing
image: debian/stable
packages:
- curl
- pkg-config
@@ -18,6 +18,12 @@ packages:
- libxinerama-dev
- libxi-dev
- libxxf86vm-dev
- libegl-mesa0
- libglx-mesa0
- libgl1-mesa-dri
- mesa-libgallium
- libgbm1
- libegl1
- mesa-vulkan-drivers
- wine
- xvfb
@@ -40,7 +46,7 @@ secrets:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl -s https://dl.google.com/go/go1.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
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 .)"
@@ -60,18 +66,13 @@ tasks:
- 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"
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" "libegl-mesa0:i386" "libglx-mesa0:i386" "libgbm1:i386" "mesa-libgallium:i386" "libgl1-mesa-dri: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_chrome: |
curl -s https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb [arch=amd64] https://dl-ssl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
sudo apt-get -qq update
sudo apt-get -qq install -y google-chrome-stable
- 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
@@ -85,6 +86,8 @@ tasks:
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: |
+1 -1
View File
@@ -10,7 +10,7 @@ environment:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.19.11.src.tar.gz | tar -C /home/build/sdk -xzf -
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: |
+17
View File
@@ -4,6 +4,7 @@ package org.gioui;
import android.app.Activity;
import android.os.Bundle;
import android.content.Intent;
import android.content.res.Configuration;
import android.view.ViewGroup;
import android.view.View;
@@ -29,6 +30,7 @@ public final class GioActivity extends Activity {
layer.addView(view);
setContentView(layer);
onNewIntent(this.getIntent());
}
@Override public void onDestroy() {
@@ -46,6 +48,16 @@ public final class GioActivity extends Activity {
super.onStop();
}
@Override public void onPause() {
super.onPause();
view.pause();
}
@Override public void onResume() {
super.onResume();
view.resume();
}
@Override public void onConfigurationChanged(Configuration c) {
super.onConfigurationChanged(c);
view.configurationChanged();
@@ -60,4 +72,9 @@ public final class GioActivity extends Activity {
if (!view.backPressed())
super.onBackPressed();
}
@Override protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
view.onIntentEvent(intent);
}
}
+28 -8
View File
@@ -12,6 +12,7 @@ import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
@@ -61,12 +62,11 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
private static boolean jniLoaded;
private final SurfaceHolder.Callback surfCallbacks;
private final View.OnFocusChangeListener focusCallback;
private final InputMethodManager imm;
private final float scrollXScale;
private final float scrollYScale;
private final AccessibilityManager accessManager;
private int keyboardHint;
private AccessibilityManager accessManager;
private long nhandle;
@@ -113,12 +113,6 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
nhandle = onCreateView(this);
setFocusable(true);
setFocusableInTouchMode(true);
focusCallback = new View.OnFocusChangeListener() {
@Override public void onFocusChange(View v, boolean focus) {
GioView.this.onFocusChange(nhandle, focus);
}
};
setOnFocusChangeListener(focusCallback);
surfCallbacks = new SurfaceHolder.Callback() {
@Override public void surfaceCreated(SurfaceHolder holder) {
// Ignore; surfaceChanged is guaranteed to be called immediately after this.
@@ -259,6 +253,10 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
}
private void setHighRefreshRate() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return;
}
Context context = getContext();
Display display = context.getDisplay();
Display.Mode[] supportedModes = display.getSupportedModes();
@@ -311,6 +309,15 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
window.setAttributes(layoutParams);
}
protected void onIntentEvent(Intent intent) {
if (intent == null) {
return;
}
if (intent.getData() != null) {
this.onOpenURI(nhandle, intent.getData().toString());
}
}
@Override protected boolean dispatchHoverEvent(MotionEvent event) {
if (!accessManager.isTouchExplorationEnabled()) {
return super.dispatchHoverEvent(event);
@@ -468,6 +475,18 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
}
}
public void pause() {
if (nhandle != 0) {
onFocusChange(nhandle, false);
}
}
public void resume() {
if (nhandle != 0) {
onFocusChange(nhandle, true);
}
}
public void destroy() {
if (nhandle != 0) {
onDestroyView(nhandle);
@@ -549,6 +568,7 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
static private native void onExitTouchExploration(long handle);
static private native void onA11yFocus(long handle, int viewId);
static private native void onClearA11yFocus(long handle, int viewId);
static private native void onOpenURI(long handle, String uri);
static private native void imeSetSnippet(long handle, int start, int end);
static private native String imeSnippet(long handle);
static private native int imeSnippetStart(long handle);
+145 -11
View File
@@ -3,9 +3,19 @@
package app
import (
"gioui.org/io/event"
"golang.org/x/net/idna"
"image"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"gioui.org/io/input"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
)
// extraArgs contains extra arguments to append to
@@ -20,23 +30,97 @@ var extraArgs string
// 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
// on X11 it is the X11 XClassHint.
//
// ID is set by the gogio tool or manually with the -X linker flag. For example,
// 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. Default value of ID is filepath.Base(os.Args[0]).
// is not supported. The default value of ID is filepath.Base(os.Args[0]).
var ID = ""
func init() {
if extraArgs != "" {
args := strings.Split(extraArgs, "|")
os.Args = append(os.Args, args...)
// 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
}
// URLEvent is generated for external requests to open a URL. Unlike window specific events,
// it is delivered through the [Events] iterator.
//
// In order to receive URLEvents the program must register one or more URL schemes. A scheme can
// be registered using gogio, with the `-schemes` flag.
type URLEvent struct {
URL *url.URL
}
// 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)
}
if ID == "" {
ID = filepath.Base(os.Args[0])
return layout.Context{
Ops: ops,
Now: e.Now,
Source: e.Source,
Metric: e.Metric,
Constraints: layout.Exact(size),
}
}
@@ -46,8 +130,7 @@ func init() {
// On iOS NSDocumentDirectory is queried.
// For Android Context.getFilesDir is used.
//
// BUG: DataDir blocks on Android until init functions
// have completed.
// BUG: On Android, DataDir panics if called before main.
func DataDir() (string, error) {
return dataDir()
}
@@ -63,3 +146,54 @@ func DataDir() (string, error) {
func Main() {
osMain()
}
// Events is an iterator that yields events that are not specific to any window,
// such as [URLEvent]. It never returns.
//
// Events must be called by the main goroutine, and replaces the
// call to [Main].
func Events(yield func(event.Event) bool) {
yieldGlobalEvent = yield
osMain()
}
var yieldGlobalEvent func(evt event.Event) bool
func processGlobalEvent(evt event.Event) {
if yieldGlobalEvent == nil {
return
}
if !yieldGlobalEvent(evt) {
yieldGlobalEvent = nil
}
}
func (FrameEvent) ImplementsEvent() {}
func (URLEvent) ImplementsEvent() {}
func init() {
if extraArgs != "" {
args := strings.Split(extraArgs, "|")
os.Args = append(os.Args, args...)
}
if ID == "" {
ID = filepath.Base(os.Args[0])
}
}
// newURLEvent creates a URLEvent from a raw URL string, handling Punycode decoding.
func newURLEvent(rawurl string) (URLEvent, error) {
u, err := url.Parse(rawurl)
if err != nil {
return URLEvent{}, err
}
u.Host, err = idna.Punycode.ToUnicode(u.Hostname())
if err != nil {
return URLEvent{}, err
}
u, err = url.Parse(u.String())
if err != nil {
return URLEvent{}, err
}
return URLEvent{URL: u}, nil
}
+5 -5
View File
@@ -60,10 +60,10 @@ func (c *d3d11Context) RenderTarget() (gpu.RenderTarget, error) {
}
func (c *d3d11Context) Present() error {
err := c.swchain.Present(1, 0)
if err == nil {
return nil
}
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:
@@ -84,7 +84,7 @@ func (c *d3d11Context) Refresh() error {
}
c.releaseFBO()
if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, 0); err != nil {
return err
return wrapErr(err)
}
c.width = width
c.height = height
-1
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build !android
// +build !android
package app
+17 -18
View File
@@ -8,21 +8,20 @@ See https://gioui.org for instructions to set up and run Gio programs.
# Windows
Create a new Window by calling NewWindow. On mobile platforms or when Gio
is embedded in another project, NewWindow merely connects with a previously
created window.
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.
A Window is run by receiving events from its Events channel. The most
important event is FrameEvent that prompts an update of the window
contents and state.
The most important event is [FrameEvent] that prompts an update of the window
contents.
For example:
import "gioui.org/unit"
w := app.NewWindow()
for e := range w.Events() {
if e, ok := e.(system.FrameEvent); ok {
w := new(app.Window)
for {
e := w.Event()
if e, ok := e.(app.FrameEvent); ok {
ops.Reset()
// Add operations to ops.
...
@@ -32,7 +31,7 @@ For example:
}
A program must keep receiving events from the event channel until
DestroyEvent is received.
[DestroyEvent] is received.
# Main
@@ -49,18 +48,18 @@ For example, to display a blank but otherwise functional window:
func main() {
go func() {
w := app.NewWindow()
for range w.Events() {
w := new(app.Window)
for {
w.Event()
}
}()
app.Main()
}
# Event queue
# Events
A FrameEvent's Queue method returns an event.Queue implementation that distributes
incoming events to the event handlers declared in the last frame.
See the gioui.org/io/event package for more information about event handlers.
The [Events] iterator yields app-specific events such as [URLEvent]. [Window.Event]
yields events that target a particular window.
# Permissions
+4 -6
View File
@@ -17,9 +17,8 @@ import (
)
type androidContext struct {
win *window
eglSurf egl.NativeWindowType
width, height int
win *window
eglSurf egl.NativeWindowType
*egl.Context
}
@@ -45,9 +44,8 @@ func (c *androidContext) Refresh() error {
if err := c.win.setVisual(c.Context.VisualID()); err != nil {
return err
}
win, width, height := c.win.nativeWindow()
win, _, _ := c.win.nativeWindow()
c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win))
c.width, c.height = width, height
return nil
}
@@ -55,7 +53,7 @@ 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, c.width, c.height); err != nil {
if err := c.Context.CreateSurface(c.eglSurf); err != nil {
return err
}
c.eglSurf = nil
+20 -13
View File
@@ -38,7 +38,24 @@ func init() {
if err != nil {
return nil, err
}
return &wlContext{Context: ctx, win: w}, nil
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
}
}
@@ -54,22 +71,12 @@ func (c *wlContext) Release() {
}
func (c *wlContext) Refresh() error {
c.Context.ReleaseSurface()
if c.eglWin != nil {
C.wl_egl_window_destroy(c.eglWin)
c.eglWin = nil
}
surf, width, height := c.win.surface()
if surf == nil {
return errors.New("wayland: no surface")
}
eglWin := C.wl_egl_window_create(surf, C.int(width), C.int(height))
if eglWin == nil {
return errors.New("wayland: wl_egl_window_create failed")
}
c.eglWin = eglWin
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
return c.Context.CreateSurface(eglSurf, width, height)
C.wl_egl_window_resize(c.eglWin, C.int(width), C.int(height), 0, 0)
return nil
}
func (c *wlContext) Lock() error {
+12 -17
View File
@@ -5,8 +5,6 @@
package app
import (
"golang.org/x/sys/windows"
"gioui.org/internal/egl"
)
@@ -24,6 +22,18 @@ func init() {
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
},
})
@@ -37,21 +47,6 @@ func (c *glContext) Release() {
}
func (c *glContext) Refresh() error {
c.Context.ReleaseSurface()
var (
win windows.Handle
width, height int
)
win, width, height = c.win.HWND()
eglSurf := egl.NativeWindowType(win)
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
return err
}
c.Context.EnableVSync(true)
c.Context.ReleaseCurrent()
return nil
}
+12 -11
View File
@@ -25,6 +25,18 @@ func init() {
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
}
}
@@ -37,17 +49,6 @@ func (c *x11Context) Release() {
}
func (c *x11Context) Refresh() error {
c.Context.ReleaseSurface()
win, width, height := c.win.window()
eglSurf := egl.NativeWindowType(uintptr(win))
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
return err
}
c.Context.EnableVSync(true)
c.Context.ReleaseCurrent()
return nil
}
-1
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build darwin && ios && nometal
// +build darwin,ios,nometal
package app
-2
View File
@@ -1,14 +1,12 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build darwin && !ios && nometal
// +build darwin,!ios,nometal
package app
import (
"errors"
"runtime"
"unsafe"
"gioui.org/gpu"
+1 -1
View File
@@ -7,7 +7,7 @@
#include <OpenGL/OpenGL.h>
#include "_cgo_export.h"
CALayer *gio_layerFactory(void) {
CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool {
return [CALayer layer];
}
+145
View File
@@ -0,0 +1,145 @@
// SPDX-License-Identifier: Unlicense OR MIT
package app
import (
"unicode"
"unicode/utf16"
"unicode/utf8"
"gioui.org/io/input"
"gioui.org/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
}
+7 -7
View File
@@ -1,18 +1,16 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build go1.18
// +build go1.18
package app
import (
"gioui.org/f32"
"testing"
"unicode/utf8"
"gioui.org/font"
"gioui.org/font/gofont"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/router"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
@@ -31,15 +29,16 @@ func FuzzIME(f *testing.F) {
f.Fuzz(func(t *testing.T, cmds []byte) {
cache := text.NewShaper(text.WithCollection(gofont.Collection()))
e := new(widget.Editor)
e.Focus()
var r router.Router
gtx := layout.Context{Ops: new(op.Ops), Queue: &r}
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
@@ -139,6 +138,7 @@ func FuzzIME(f *testing.F) {
func TestEditorIndices(t *testing.T) {
var s editorState
s.Selection.Transform = f32.AffineId()
const str = "Hello, 😀"
s.Snippet = key.Snippet{
Text: str,
-7
View File
@@ -1,7 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package points standard output, standard error and the standard
// library package log to the platform logger.
package log
var appID = "gio"
+178 -40
View File
@@ -108,6 +108,80 @@ type MonitorInfo struct {
Flags uint32
}
type CopyDataStruct struct {
DwData uintptr
CbData uint32
LpData uintptr
}
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
@@ -244,44 +318,52 @@ const (
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_QUIT = 0x0012
WM_SETCURSOR = 0x0020
WM_SETFOCUS = 0x0007
WM_SHOWWINDOW = 0x0018
WM_SIZE = 0x0005
WM_SYSKEYDOWN = 0x0104
WM_SYSKEYUP = 0x0105
WM_RBUTTONDOWN = 0x0204
WM_RBUTTONUP = 0x0205
WM_TIMER = 0x0113
WM_UNICHAR = 0x0109
WM_USER = 0x0400
WM_WINDOWPOSCHANGED = 0x0047
WM_CANCELMODE = 0x001F
WM_CHAR = 0x0102
WM_CLOSE = 0x0010
WM_COPYDATA = 0x004A
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
@@ -344,7 +426,9 @@ var (
_DefWindowProc = user32.NewProc("DefWindowProcW")
_DestroyWindow = user32.NewProc("DestroyWindow")
_DispatchMessage = user32.NewProc("DispatchMessageW")
_FindWindow = user32.NewProc("FindWindowW")
_EmptyClipboard = user32.NewProc("EmptyClipboard")
_EnableMouseInPointer = user32.NewProc("EnableMouseInPointer")
_GetWindowRect = user32.NewProc("GetWindowRect")
_GetClientRect = user32.NewProc("GetClientRect")
_GetClipboardData = user32.NewProc("GetClipboardData")
@@ -354,6 +438,7 @@ var (
_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")
@@ -371,9 +456,11 @@ var (
_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")
_SendMessage = user32.NewProc("SendMessageW")
_SetCapture = user32.NewProc("SetCapture")
_SetCursor = user32.NewProc("SetCursor")
_SetClipboardData = user32.NewProc("SetClipboardData")
@@ -426,7 +513,10 @@ func CloseClipboard() error {
}
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)
wname, err := syscall.UTF16PtrFromString(lpWindowName)
if err != nil {
return 0, fmt.Errorf("CreateWindowEx failed: %v", err)
}
hwnd, _, err := _CreateWindowEx.Call(
uintptr(dwExStyle),
uintptr(lpClassName),
@@ -444,6 +534,31 @@ func CreateWindowEx(dwExStyle uint32, lpClassName uint16, lpWindowName string, d
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
@@ -473,6 +588,18 @@ func EmptyClipboard() error {
return nil
}
func FindWindow(lpClassName string) (syscall.Handle, error) {
className, err := syscall.UTF16PtrFromString(lpClassName)
if err != nil {
return 0, fmt.Errorf("FindWindow failed: %v", err)
}
hwnd, _, err := _FindWindow.Call(uintptr(unsafe.Pointer(className)), 0)
if hwnd == 0 {
return 0, fmt.Errorf("FindWindow failed: %v", err)
}
return syscall.Handle(hwnd), nil
}
func GetWindowRect(hwnd syscall.Handle) Rect {
var r Rect
_GetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r)))
@@ -664,7 +791,10 @@ func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int3
}
func SetWindowText(hwnd syscall.Handle, title string) {
wname := syscall.StringToUTF16Ptr(title)
wname, err := syscall.UTF16PtrFromString(title)
if err != nil {
panic(err)
}
_SetWindowText.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wname)))
}
@@ -780,6 +910,14 @@ func ReleaseDC(hdc syscall.Handle) {
_ReleaseDC.Call(uintptr(hdc))
}
func SendMessage(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) error {
r, _, err := _SendMessage.Call(uintptr(hwnd), uintptr(msg), wParam, lParam)
if r == 0 {
return fmt.Errorf("SendMessage failed: %v", err)
}
return nil
}
func SetForegroundWindow(hwnd syscall.Handle) {
_SetForegroundWindow.Call(uintptr(hwnd))
}
+5 -5
View File
@@ -238,17 +238,17 @@ func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latched
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) (string, bool) {
func convertKeysym(s C.xkb_keysym_t) (key.Name, bool) {
if 'a' <= s && s <= 'z' {
return string(rune(s - 'a' + 'A')), true
return key.Name(rune(s - 'a' + 'A')), true
}
if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 {
return string(rune(s - C.XKB_KEY_KP_0 + '0')), true
return key.Name(rune(s - C.XKB_KEY_KP_0 + '0')), true
}
if ' ' < s && s <= '~' {
return string(rune(s)), true
return key.Name(rune(s)), true
}
var n string
var n key.Name
switch s {
case C.XKB_KEY_Escape:
n = key.NameEscape
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
package log
package app
/*
#cgo LDFLAGS: -llog
@@ -22,7 +22,7 @@ import (
// 1024 is the truncation limit from android/log.h, plus a \n.
const logLineLimit = 1024
var logTag = C.CString(appID)
var logTag = C.CString(ID)
func init() {
// Android's logcat already includes timestamps.
@@ -1,9 +1,8 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build darwin && ios
// +build darwin,ios
package log
package app
/*
#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c
@@ -1,17 +1,18 @@
// SPDX-License-Identifier: Unlicense OR MIT
package log
package app
import (
"log"
"syscall"
"unsafe"
syscall "golang.org/x/sys/windows"
)
type logger struct{}
var (
kernel32 = syscall.NewLazyDLL("kernel32")
kernel32 = syscall.NewLazySystemDLL("kernel32")
outputDebugStringW = kernel32.NewProc("OutputDebugStringW")
debugView *logger
)
+3 -2
View File
@@ -60,8 +60,9 @@ static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) {
id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
[cmdBuffer presentDrawable:drawable];
[cmdBuffer commit];
[cmdBuffer waitUntilScheduled];
[drawable present];
}
}
@@ -95,7 +96,7 @@ func newMtlContext(w *window) (*mtlContext, error) {
return nil, errors.New("metal: CAMetalLayer construction failed")
}
queue := C.newCommandQueue(dev)
if layer == 0 {
if queue == 0 {
C.CFRelease(dev)
C.CFRelease(layer)
return nil, errors.New("metal: [MTLDevice newCommandQueue] failed")
+4 -2
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build !nometal
// +build !nometal
package app
@@ -21,7 +20,10 @@ Class gio_layerClass(void) {
static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
@autoreleasepool {
UIView *view = (__bridge UIView *)viewRef;
return CFBridgingRetain(view.layer);
CAMetalLayer *l = (CAMetalLayer *)view.layer;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = YES;
return CFBridgingRetain(l);
}
}
+6 -2
View File
@@ -12,9 +12,13 @@ package app
#import <QuartzCore/CAMetalLayer.h>
#include <CoreFoundation/CoreFoundation.h>
CALayer *gio_layerFactory(void) {
CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool {
return [CAMetalLayer layer];
CAMetalLayer *l = [CAMetalLayer layer];
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = presentWithTrans;
return l;
}
}
+166 -22
View File
@@ -7,7 +7,9 @@ import (
"image"
"image/color"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/op"
"gioui.org/gpu"
"gioui.org/io/pointer"
@@ -43,6 +45,10 @@ type Config struct {
CustomRenderer bool
// Decorated reports whether window decorations are provided automatically.
Decorated bool
// TopMost windows render above all other non-top-most windows.
TopMost bool
// Focused reports whether the window is focused.
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
@@ -131,8 +137,30 @@ func (o Orientation) String() string {
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 {
system.FrameEvent
FrameEvent
Sync bool
}
@@ -147,9 +175,13 @@ type context interface {
Unlock()
}
// Driver is the interface for the platform implementation
// 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)
@@ -160,23 +192,27 @@ type driver interface {
// ReadClipboard requests the clipboard content.
ReadClipboard()
// WriteClipboard requests a clipboard write.
WriteClipboard(s string)
WriteClipboard(mime string, s []byte)
// Configure the window.
Configure([]Option)
// SetCursor updates the current cursor to name.
SetCursor(cursor pointer.Cursor)
// Wakeup wakes up the event loop and sends a WakeupEvent.
Wakeup()
// 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
errs chan error
in chan windowAndConfig
out chan windowAndConfig
windows chan struct{}
}
type windowAndConfig struct {
@@ -186,32 +222,137 @@ type windowAndConfig struct {
func newWindowRendezvous() *windowRendezvous {
wr := &windowRendezvous{
in: make(chan windowAndConfig),
out: make(chan windowAndConfig),
errs: make(chan error),
in: make(chan windowAndConfig),
out: make(chan windowAndConfig),
windows: make(chan struct{}),
}
go func() {
var main windowAndConfig
in := wr.in
var window windowAndConfig
var out chan windowAndConfig
for {
select {
case w := <-wr.in:
var err error
if main.window != nil {
err = errors.New("multiple windows are not supported")
}
wr.errs <- err
main = w
case w := <-in:
window = w
out = wr.out
case out <- main:
case out <- window:
}
}
}()
return wr
}
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
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 {
@@ -221,3 +362,6 @@ func walkActions(actions system.Action, do func(system.Action)) {
}
}
}
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
+164 -113
View File
@@ -123,24 +123,29 @@ import (
"fmt"
"image"
"image/color"
"io"
"math"
"os"
"path/filepath"
"runtime"
"runtime/cgo"
"runtime/debug"
"strings"
"sync"
"time"
"unicode/utf16"
"unsafe"
"gioui.org/io/transfer"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/unit"
@@ -148,6 +153,7 @@ import (
type window struct {
callbacks *callbacks
loop *eventLoop
view C.jobject
handle cgo.Handle
@@ -156,18 +162,19 @@ type window struct {
fontScale float32
insets pixelInsets
stage system.Stage
visible bool
started bool
animating bool
win *C.ANativeWindow
config Config
win *C.ANativeWindow
config Config
inputHint key.InputHint
semantic struct {
hoverID router.SemanticID
rootID router.SemanticID
focusID router.SemanticID
diffs []router.SemanticID
hoverID input.SemanticID
rootID input.SemanticID
focusID input.SemanticID
diffs []input.SemanticID
}
}
@@ -199,9 +206,9 @@ type pixelInsets struct {
top, bottom, left, right int
}
// ViewEvent is sent whenever the Window's underlying Android view
// AndroidViewEvent is sent whenever the Window's underlying Android view
// changes.
type ViewEvent struct {
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.
@@ -211,8 +218,6 @@ type ViewEvent struct {
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
@@ -288,8 +293,7 @@ var mainWindow = newWindowRendezvous()
var mainFuncs = make(chan func(env *C.JNIEnv), 1)
var (
dataDirOnce sync.Once
dataPath string
dataPath string
)
var (
@@ -336,9 +340,9 @@ func (w *window) NewContext() (context, error) {
}
func dataDir() (string, error) {
dataDirOnce.Do(func() {
dataPath = <-dataDirChan
})
if dataPath == "" {
panic("DataDir isn't valid before main")
}
return dataPath, nil
}
@@ -391,7 +395,7 @@ func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, jdataDir C.jbyt
os.Setenv("HOME", dataDir)
}
dataDirChan <- dataDir
dataPath = dataDir
C.jni_ReleaseByteArrayElements(env, jdataDir, dirBytes)
runMain()
@@ -485,24 +489,30 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
})
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.callbacks.SetDriver(w)
w.loadConfig(env, class)
w.Configure(wopts.options)
w.SetInputHint(key.HintAny)
w.setStage(system.StagePaused)
w.callbacks.Event(ViewEvent{View: uintptr(view)})
w.setConfig(env, cnf)
w.SetInputHint(w.inputHint)
w.processEvent(AndroidViewEvent{View: uintptr(view)})
return C.jlong(w.handle)
}
@@ -516,7 +526,7 @@ func Java_org_gioui_GioView_onDestroyView(env *C.JNIEnv, class C.jclass, handle
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.setStage(system.StagePaused)
w.visible = false
}
//export Java_org_gioui_GioView_onStartView
@@ -532,7 +542,7 @@ func Java_org_gioui_GioView_onStartView(env *C.JNIEnv, class C.jclass, handle C.
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.setStage(system.StagePaused)
w.visible = false
}
//export Java_org_gioui_GioView_onSurfaceChanged
@@ -554,9 +564,7 @@ func Java_org_gioui_GioView_onLowMemory(env *C.JNIEnv, class C.jclass) {
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)
if w.stage >= system.StageInactive {
w.draw(env, true)
}
w.draw(env, true)
}
//export Java_org_gioui_GioView_onFrameCallback
@@ -565,19 +573,13 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
if !exist {
return
}
if w.stage < system.StageInactive {
return
}
if w.animating {
w.draw(env, false)
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
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.callbacks.Event(key.Event{Name: key.NameBack}) {
if w.processEvent(key.Event{Name: key.NameBack}) {
return C.JNI_TRUE
}
return C.JNI_FALSE
@@ -586,7 +588,8 @@ func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong)
//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.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE})
w.config.Focused = focus == C.JNI_TRUE
w.processEvent(ConfigEvent{Config: w.config})
}
//export Java_org_gioui_GioView_onWindowInsets
@@ -598,9 +601,7 @@ func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C
left: int(left),
right: int(right),
}
if w.stage >= system.StageInactive {
w.draw(env, true)
}
w.draw(env, true)
}
//export Java_org_gioui_GioView_initializeAccessibilityNodeInfo
@@ -661,7 +662,44 @@ func Java_org_gioui_GioView_onClearA11yFocus(env *C.JNIEnv, class C.jclass, view
}
}
func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNode, off image.Point, info C.jobject) error {
//export Java_org_gioui_GioView_onOpenURI
func Java_org_gioui_GioView_onOpenURI(env *C.JNIEnv, class C.jclass, view C.jlong, uri C.jstring) {
evt, err := newURLEvent(goString(env, uri))
if err != nil {
return
}
processGlobalEvent(evt)
}
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 {
@@ -704,7 +742,7 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNod
panic(err)
}
}
if d.Gestures&router.ClickGesture != 0 {
if d.Gestures&input.ClickGesture != 0 {
addAction(ACTION_CLICK)
}
clsName := android.strings.androidViewView
@@ -749,25 +787,23 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNod
return nil
}
func (w *window) virtualIDFor(id router.SemanticID) C.jint {
// TODO: Android virtual IDs are 32-bit Java integers, but childID is a int64.
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) router.SemanticID {
func (w *window) semIDFor(virtID C.jint) input.SemanticID {
if virtID == HOST_VIEW_ID {
return w.semantic.rootID
}
return router.SemanticID(virtID)
return input.SemanticID(virtID)
}
func (w *window) detach(env *C.JNIEnv) {
callVoidMethod(env, w.view, gioView.unregister)
w.callbacks.Event(ViewEvent{})
w.callbacks.SetDriver(nil)
w.processEvent(AndroidViewEvent{})
w.handle.Delete()
C.jni_DeleteGlobalRef(env, w.view)
w.view = 0
@@ -778,18 +814,10 @@ func (w *window) setVisible(env *C.JNIEnv) {
if width == 0 || height == 0 {
return
}
w.setStage(system.StageRunning)
w.visible = true
w.draw(env, true)
}
func (w *window) setStage(stage system.Stage) {
if stage == w.stage {
return
}
w.stage = stage
w.callbacks.Event(system.StageEvent{stage})
}
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")
@@ -826,10 +854,13 @@ func (w *window) SetAnimating(anim bool) {
}
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.callbacks.Event(ConfigEvent{Config: w.config})
w.processEvent(ConfigEvent{Config: w.config})
}
if size.X == 0 || size.Y == 0 {
return
@@ -837,14 +868,14 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
const inchPrDp = 1.0 / 160
ppdp := float32(w.dpi) * inchPrDp
dppp := unit.Dp(1.0 / ppdp)
insets := system.Insets{
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.callbacks.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Insets: insets,
@@ -855,6 +886,9 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
},
Sync: sync,
})
if w.animating {
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
a11yActive, err := callBooleanMethod(env, w.view, gioView.isA11yActive)
if err != nil {
panic(err)
@@ -898,8 +932,8 @@ func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
f(env)
}
func convertKeyCode(code C.jint) (string, bool) {
var n string
func convertKeyCode(code C.jint) (key.Name, bool) {
var n key.Name
switch code {
case C.AKEYCODE_FORWARD_DEL:
n = key.NameDeleteForward
@@ -943,7 +977,7 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
if pressed == C.JNI_TRUE {
state = key.Press
}
w.callbacks.Event(key.Event{Name: n, State: state})
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)))
@@ -953,18 +987,18 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
//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 typ pointer.Type
var kind pointer.Kind
switch action {
case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN:
typ = pointer.Press
kind = pointer.Press
case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP:
typ = pointer.Release
kind = pointer.Release
case C.AMOTION_EVENT_ACTION_CANCEL:
typ = pointer.Cancel
kind = pointer.Cancel
case C.AMOTION_EVENT_ACTION_MOVE:
typ = pointer.Move
kind = pointer.Move
case C.AMOTION_EVENT_ACTION_SCROLL:
typ = pointer.Scroll
kind = pointer.Scroll
default:
return
}
@@ -993,8 +1027,8 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
default:
return
}
w.callbacks.Event(pointer.Event{
Type: typ,
w.processEvent(pointer.Event{
Kind: kind,
Source: src,
Buttons: btns,
PointerID: pointer.ID(pointerID),
@@ -1145,6 +1179,8 @@ func (w *window) ShowTextInput(show bool) {
}
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
@@ -1289,16 +1325,17 @@ func findClass(env *C.JNIEnv, name string) C.jclass {
}
func osMain() {
select {}
}
func newWindow(window *callbacks, options []Option) error {
func newWindow(window *callbacks, options []Option) {
mainWindow.in <- windowAndConfig{window, options}
return <-mainWindow.errs
<-mainWindow.windows
}
func (w *window) WriteClipboard(s string) {
func (w *window) WriteClipboard(mime string, s []byte) {
runInJVM(javaVM(), func(env *C.JNIEnv) {
jstr := javaString(env, s)
jstr := javaString(env, string(s))
callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
jvalue(android.appCtx), jvalue(jstr))
})
@@ -1312,47 +1349,56 @@ func (w *window) ReadClipboard() {
return
}
content := goString(env, C.jstring(c))
w.callbacks.Event(clipboard.Event{Text: content})
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) {
prev := w.config
cnf := w.config
cnf.apply(unit.Metric{}, options)
// 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.callbacks.Event(ConfigEvent{Config: w.config})
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) {
@@ -1361,9 +1407,10 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
})
}
func (w *window) Wakeup() {
func (w *window) wakeup() {
runOnMain(func(env *C.JNIEnv) {
w.callbacks.Event(wakeupEvent{})
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
@@ -1454,4 +1501,8 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
}
}
func (_ ViewEvent) ImplementsEvent() {}
func (AndroidViewEvent) implementsViewEvent() {}
func (AndroidViewEvent) ImplementsEvent() {}
func (a AndroidViewEvent) Valid() bool {
return a != (AndroidViewEvent{})
}
+18 -20
View File
@@ -5,7 +5,7 @@ package app
/*
#include <Foundation/Foundation.h>
__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
__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);
@@ -40,8 +40,10 @@ static CFTypeRef newNSString(unichar *chars, NSUInteger length) {
}
*/
import "C"
import (
"errors"
"runtime/cgo"
"sync"
"sync/atomic"
"time"
@@ -73,30 +75,25 @@ type displayLink struct {
// displayLinks maps CFTypeRefs to *displayLinks.
var displayLinks sync.Map
var mainFuncs = make(chan func(), 1)
func isMainThread() bool {
return bool(C.isMainThread())
}
// runOnMain runs the function on the main thread.
func runOnMain(f func()) {
if C.isMainThread() {
if isMainThread() {
f()
return
}
go func() {
mainFuncs <- f
C.gio_wakeupMainThread()
}()
C.gio_runOnMain(C.uintptr_t(cgo.NewHandle(f)))
}
//export gio_dispatchMainFuncs
func gio_dispatchMainFuncs() {
for {
select {
case f := <-mainFuncs:
f()
default:
return
}
}
//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.
@@ -124,7 +121,7 @@ func stringToNSString(str string) C.CFTypeRef {
return C.newNSString(chars, C.NSUInteger(len(u16)))
}
func NewDisplayLink(callback func()) (*displayLink, error) {
func newDisplayLink(callback func()) (*displayLink, error) {
d := &displayLink{
callback: callback,
done: make(chan struct{}),
@@ -260,8 +257,9 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
return to
}
func (w *window) Wakeup() {
func (w *window) wakeup() {
runOnMain(func() {
w.w.Event(wakeupEvent{})
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
+2 -2
View File
@@ -4,8 +4,8 @@
#include "_cgo_export.h"
void gio_wakeupMainThread(void) {
void gio_runOnMain(uintptr_t h) {
dispatch_async(dispatch_get_main_queue(), ^{
gio_dispatchMainFuncs();
gio_runFunc(h);
});
}
+171 -74
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build darwin && ios
// +build darwin,ios
package app
@@ -12,6 +11,9 @@ package app
#include <UIKit/UIKit.h>
#include <stdint.h>
__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;
@@ -19,6 +21,7 @@ struct drawParams {
};
static void writeClipboard(unichar *chars, NSUInteger length) {
#if !TARGET_OS_TV
@autoreleasepool {
NSString *s = [NSString string];
if (length > 0) {
@@ -27,13 +30,18 @@ static void writeClipboard(unichar *chars, NSUInteger 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) {
@@ -72,21 +80,27 @@ import "C"
import (
"image"
"io"
"os"
"runtime"
"runtime/cgo"
"runtime/debug"
"strings"
"time"
"unicode/utf16"
"unsafe"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
)
type ViewEvent struct {
type UIKitViewEvent struct {
// ViewController is a CFTypeRef for the UIViewController backing a Window.
ViewController uintptr
}
@@ -95,18 +109,17 @@ type window struct {
view C.CFTypeRef
w *callbacks
displayLink *displayLink
loop *eventLoop
visible bool
cursor pointer.Cursor
config Config
hidden bool
cursor pointer.Cursor
config Config
pointerMap []C.CFTypeRef
}
var mainWindow = newWindowRendezvous()
var views = make(map[C.CFTypeRef]*window)
func init() {
// Darwin requires UI operations happen on the main thread only.
runtime.LockOSThread()
@@ -114,55 +127,59 @@ func init() {
//export onCreate
func onCreate(view, controller C.CFTypeRef) {
wopts := <-mainWindow.out
w := &window{
view: view,
w: wopts.window,
}
dl, err := NewDisplayLink(func() {
w.loop = newEventLoop(w.w, w.wakeup)
w.w.SetDriver(w)
mainWindow.windows <- struct{}{}
dl, err := newDisplayLink(func() {
w.draw(false)
})
if err != nil {
panic(err)
w.w.ProcessEvent(DestroyEvent{Err: err})
return
}
w.displayLink = dl
wopts := <-mainWindow.out
w.w = wopts.window
w.w.SetDriver(w)
views[view] = w
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
w.Configure(wopts.options)
w.w.Event(system.StageEvent{Stage: system.StagePaused})
w.w.Event(ViewEvent{ViewController: uintptr(controller)})
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(view C.CFTypeRef) {
w := views[view]
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
}
wasVisible := w.visible
w.visible = true
if !wasVisible {
w.w.Event(system.StageEvent{Stage: system.StageRunning})
}
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.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: image.Point{
X: int(params.width + .5),
Y: int(params.height + .5),
},
Insets: system.Insets{
Insets: Insets{
Top: unit.Dp(params.top) * dppp,
Bottom: unit.Dp(params.bottom) * dppp,
Left: unit.Dp(params.left) * dppp,
@@ -175,26 +192,34 @@ func (w *window) draw(sync bool) {
}
//export onStop
func onStop(view C.CFTypeRef) {
w := views[view]
w.visible = false
w.w.Event(system.StageEvent{Stage: system.StagePaused})
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(view C.CFTypeRef) {
w := views[view]
delete(views, view)
w.w.Event(ViewEvent{})
w.w.Event(system.DestroyEvent{})
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(view C.CFTypeRef, focus int) {
w := views[view]
w.w.Event(key.FocusEvent{Focus: focus != 0})
func onFocus(h C.uintptr_t, focus int) {
w := viewFor(h)
w.config.Focused = focus != 0
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export onLowMemory
@@ -204,56 +229,56 @@ func onLowMemory() {
}
//export onUpArrow
func onUpArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameUpArrow)
func onUpArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameUpArrow)
}
//export onDownArrow
func onDownArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameDownArrow)
func onDownArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameDownArrow)
}
//export onLeftArrow
func onLeftArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameLeftArrow)
func onLeftArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameLeftArrow)
}
//export onRightArrow
func onRightArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameRightArrow)
func onRightArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameRightArrow)
}
//export onDeleteBackward
func onDeleteBackward(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameDeleteBackward)
func onDeleteBackward(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameDeleteBackward)
}
//export onText
func onText(view, str C.CFTypeRef) {
w := views[view]
func onText(h C.uintptr_t, str C.CFTypeRef) {
w := viewFor(h)
w.w.EditorInsert(nsstringToString(str))
}
//export onTouch
func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
var typ pointer.Type
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:
typ = pointer.Press
kind = pointer.Press
case C.UITouchPhaseMoved:
typ = pointer.Move
kind = pointer.Move
case C.UITouchPhaseEnded:
typ = pointer.Release
kind = pointer.Release
case C.UITouchPhaseCancelled:
typ = pointer.Cancel
kind = pointer.Cancel
default:
return
}
w := views[view]
w := viewFor(h)
t := time.Duration(float64(ti) * float64(time.Second))
p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{
Type: typ,
w.ProcessEvent(pointer.Event{
Kind: kind,
Source: pointer.Touch,
PointerID: w.lookupTouch(last != 0, touchRef),
Position: p,
@@ -265,11 +290,16 @@ func (w *window) ReadClipboard() {
cstr := C.readClipboard()
defer C.CFRelease(cstr)
content := nsstringToString(cstr)
w.w.Event(clipboard.Event{Text: content})
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
}
func (w *window) WriteClipboard(s string) {
u16 := utf16.Encode([]rune(s))
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]))
@@ -280,7 +310,7 @@ func (w *window) WriteClipboard(s string) {
func (w *window) Configure([]Option) {
// Decorations are never disabled.
w.config.Decorated = true
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *window) EditorStateChanged(old, new editorState) {}
@@ -288,10 +318,6 @@ func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) Perform(system.Action) {}
func (w *window) SetAnimating(anim bool) {
v := w.view
if v == 0 {
return
}
if anim {
w.displayLink.Start()
} else {
@@ -303,8 +329,8 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
w.cursor = windowSetCursor(w.cursor, cursor)
}
func (w *window) onKeyCommand(name string) {
w.w.Event(key.Event{
func (w *window) onKeyCommand(name key.Name) {
w.ProcessEvent(key.Event{
Name: name,
})
}
@@ -343,17 +369,88 @@ func (w *window) ShowTextInput(show bool) {
func (w *window) SetInputHint(_ key.InputHint) {}
func newWindow(win *callbacks, options []Option) error {
mainWindow.in <- windowAndConfig{win, options}
return <-mainWindow.errs
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() {
switch mainMode {
case mainModeUndefined:
if !isMainThread() {
panic("app.Main must be run on the main goroutine")
}
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.
}
select {}
}
//export gio_onOpenURI
func gio_onOpenURI(uri C.CFTypeRef) {
evt, err := newURLEvent(nsstringToString(uri))
if err != nil {
return
}
processGlobalEvent(evt)
}
//export gio_runMain
func gio_runMain() {
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 (_ ViewEvent) ImplementsEvent() {}
func (UIKitViewEvent) implementsViewEvent() {}
func (UIKitViewEvent) ImplementsEvent() {}
func (u UIKitViewEvent) Valid() bool {
return u != (UIKitViewEvent{})
}
+102 -33
View File
@@ -11,6 +11,7 @@
__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
@interface GioView: UIView <UIKeyInput>
@property uintptr_t handle;
@end
@implementation GioViewController
@@ -25,12 +26,13 @@ CGFloat _keyboardHeight;
self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame];
[self.view addSubview: drawView];
#ifndef TARGET_OS_TV
#if !TARGET_OS_TV
drawView.multipleTouchEnabled = YES;
#endif
drawView.preservesSuperviewLayoutMargins = YES;
drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
onCreate((__bridge CFTypeRef)drawView, (__bridge CFTypeRef)self);
#if !TARGET_OS_TV
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChange:)
name:UIKeyboardWillShowNotification
@@ -43,6 +45,7 @@ CGFloat _keyboardHeight;
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
#endif
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(applicationDidEnterBackground:)
name: UIApplicationDidEnterBackgroundNotification
@@ -54,33 +57,33 @@ CGFloat _keyboardHeight;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
UIView *drawView = self.view.subviews[0];
if (drawView != nil) {
gio_onDraw((__bridge CFTypeRef)drawView);
GioView *view = (GioView *)self.view.subviews[0];
if (view != nil) {
onStart(view.handle);
}
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
UIView *drawView = self.view.subviews[0];
if (drawView != nil) {
onStop((__bridge CFTypeRef)drawView);
GioView *view = (GioView *)self.view.subviews[0];
if (view != nil) {
onStop(view.handle);
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
CFTypeRef viewRef = (__bridge CFTypeRef)self.view.subviews[0];
onDestroy(viewRef);
GioView *view = (GioView *)self.view.subviews[0];
onDestroy(view.handle);
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
UIView *view = self.view.subviews[0];
GioView *view = (GioView *)self.view.subviews[0];
CGRect frame = self.view.bounds;
// Adjust view bounds to make room for the keyboard.
frame.size.height -= _keyboardHeight;
view.frame = frame;
gio_onDraw((__bridge CFTypeRef)view);
gio_onDraw(view.handle);
}
- (void)didReceiveMemoryWarning {
@@ -88,6 +91,7 @@ CGFloat _keyboardHeight;
[super didReceiveMemoryWarning];
}
#if !TARGET_OS_TV
- (void)keyboardWillChange:(NSNotification *)note {
NSDictionary *userInfo = note.userInfo;
CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
@@ -99,13 +103,13 @@ CGFloat _keyboardHeight;
_keyboardHeight = 0.0;
[self.view setNeedsLayout];
}
#endif
@end
static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIEvent *event) {
static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UIEvent *event) {
CGFloat scale = view.contentScaleFactor;
NSUInteger i = 0;
NSUInteger n = [touches count];
CFTypeRef viewRef = (__bridge CFTypeRef)view;
for (UITouch *touch in touches) {
CFTypeRef touchRef = (__bridge CFTypeRef)touch;
i++;
@@ -116,7 +120,7 @@ static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIE
CGPoint loc = [coalescedTouch locationInView:view];
j++;
int lastTouch = last && i == n && j == m;
onTouch(lastTouch, viewRef, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
onTouch(view.handle, lastTouch, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
}
}
}
@@ -130,34 +134,70 @@ NSArray<UIKeyCommand *> *_keyCommands;
return gio_layerClass();
}
- (void)willMoveToWindow:(UIWindow *)newWindow {
self.contentScaleFactor = newWindow.screen.nativeScale;
if (@available(iOS 13.0, *)) {
[self registerSceneNotifications:newWindow];
}else{
[self registerWindowNotifications:newWindow];
}
}
- (void)registerSceneNotifications:(UIWindow *)newWindow {
if (self.window != nil) {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UISceneDidActivateNotification
object:self.window.windowScene];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UISceneWillDeactivateNotification
object:self.window.windowScene];
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onSceneDidActivate:)
name:UISceneDidActivateNotification
object:newWindow.windowScene];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onSceneWillDeactivate:)
name:UISceneWillDeactivateNotification
object:newWindow.windowScene];
}
- (void)onSceneDidActivate:(NSNotification *)note API_AVAILABLE(ios(13.0)){
onFocus(self.handle, YES);
}
- (void)onSceneWillDeactivate:(NSNotification *)note API_AVAILABLE(ios(13.0)){
onFocus(self.handle, NO);
}
- (void)registerWindowNotifications:(UIWindow *)newWindow {
if (self.window != nil) {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIWindowDidBecomeKeyNotification
object:self.window];
name:UIWindowDidBecomeKeyNotification
object:self.window];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIWindowDidResignKeyNotification
object:self.window];
name:UIWindowDidResignKeyNotification
object:self.window];
}
self.contentScaleFactor = newWindow.screen.nativeScale;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onWindowDidBecomeKey:)
name:UIWindowDidBecomeKeyNotification
object:newWindow];
selector:@selector(onWindowDidBecomeKey:)
name:UIWindowDidBecomeKeyNotification
object:newWindow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onWindowDidResignKey:)
name:UIWindowDidResignKeyNotification
object:newWindow];
selector:@selector(onWindowDidResignKey:)
name:UIWindowDidResignKeyNotification
object:newWindow];
}
- (void)onWindowDidBecomeKey:(NSNotification *)note {
if (self.isFirstResponder) {
onFocus((__bridge CFTypeRef)self, YES);
onFocus(self.handle, YES);
}
}
- (void)onWindowDidResignKey:(NSNotification *)note {
if (self.isFirstResponder) {
onFocus((__bridge CFTypeRef)self, NO);
onFocus(self.handle, NO);
}
}
@@ -178,7 +218,7 @@ NSArray<UIKeyCommand *> *_keyCommands;
}
- (void)insertText:(NSString *)text {
onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)text);
onText(self.handle, (__bridge CFTypeRef)text);
}
- (BOOL)canBecomeFirstResponder {
@@ -190,23 +230,23 @@ NSArray<UIKeyCommand *> *_keyCommands;
}
- (void)deleteBackward {
onDeleteBackward((__bridge CFTypeRef)self);
onDeleteBackward(self.handle);
}
- (void)onUpArrow {
onUpArrow((__bridge CFTypeRef)self);
onUpArrow(self.handle);
}
- (void)onDownArrow {
onDownArrow((__bridge CFTypeRef)self);
onDownArrow(self.handle);
}
- (void)onLeftArrow {
onLeftArrow((__bridge CFTypeRef)self);
onLeftArrow(self.handle);
}
- (void)onRightArrow {
onRightArrow((__bridge CFTypeRef)self);
onRightArrow(self.handle);
}
- (NSArray<UIKeyCommand *> *)keyCommands {
@@ -271,3 +311,32 @@ void gio_showCursor() {
void gio_setCursor(NSUInteger curID) {
// Not supported.
}
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
GioView *v = (__bridge GioView *)viewRef;
v.handle = handle;
}
@interface _gioAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end
@implementation _gioAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = controller;
[self.window makeKeyAndVisible];
return YES;
}
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
gio_onOpenURI((__bridge CFTypeRef)url.absoluteString);
return YES;
}
@end
int gio_applicationMain(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([_gioAppDelegate class]));
}
}
+452 -115
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"image"
"image/color"
"io"
"strings"
"syscall/js"
"time"
@@ -13,16 +14,18 @@ import (
"unicode/utf8"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit"
)
type ViewEvent struct {
type JSViewEvent struct {
Element js.Value
}
@@ -50,12 +53,10 @@ type window struct {
screenOrientation js.Value
cleanfuncs []func()
touches []js.Value
composing bool
composing int
lastCursor int
requestFocus bool
chanAnimation chan struct{}
chanRedraw chan struct{}
config Config
inset f32.Point
scale float32
@@ -68,7 +69,7 @@ type window struct {
contextStatus contextStatus
}
func newWindow(win *callbacks, options []Option) error {
func newWindow(win *callbacks, options []Option) {
doc := js.Global().Get("document")
cont := getContainer(doc)
cnv := createCanvas(doc)
@@ -83,7 +84,10 @@ func newWindow(win *callbacks, options []Option) error {
head: doc.Get("head"),
clipboard: js.Global().Get("navigator").Get("clipboard"),
wakeups: make(chan struct{}, 1),
w: win,
composing: -1,
}
w.w.SetDriver(w)
w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
w.browserHistory = w.window.Get("history")
w.visualViewport = w.window.Get("visualViewport")
@@ -93,42 +97,28 @@ func newWindow(win *callbacks, options []Option) error {
if screen := w.window.Get("screen"); screen.Truthy() {
w.screenOrientation = screen.Get("orientation")
}
w.chanAnimation = make(chan struct{}, 1)
w.chanRedraw = make(chan struct{}, 1)
w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
w.chanAnimation <- struct{}{}
w.draw(false)
return nil
})
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
content := args[0].String()
go win.Event(clipboard.Event{Text: content})
w.processEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
return nil
})
w.addEventListeners()
w.addHistory()
w.w = win
go func() {
defer w.cleanup()
w.w.SetDriver(w)
w.Configure(options)
w.blur()
w.w.Event(ViewEvent{Element: cont})
w.w.Event(system.StageEvent{Stage: system.StageRunning})
w.resize()
w.draw(true)
for {
select {
case <-w.wakeups:
w.w.Event(wakeupEvent{})
case <-w.chanAnimation:
w.animCallback()
case <-w.chanRedraw:
w.draw(true)
}
}
}()
return nil
w.Configure(options)
w.blur()
w.processEvent(JSViewEvent{Element: cont})
w.resize()
w.draw(true)
}
func getContainer(doc js.Value) js.Value {
@@ -142,8 +132,10 @@ func getContainer(doc js.Value) js.Value {
}
func createTextArea(doc js.Value) js.Value {
tarea := doc.Call("createElement", "input")
tarea := doc.Call("createElement", "textarea")
style := tarea.Get("style")
// Position absolute so left/top coordinates actually place the element
style.Set("position", "absolute")
style.Set("width", "1px")
style.Set("height", "1px")
style.Set("opacity", "0")
@@ -153,6 +145,12 @@ func createTextArea(doc js.Value) js.Value {
tarea.Set("autocorrect", "off")
tarea.Set("autocapitalize", "off")
tarea.Set("spellcheck", false)
// Enable multiline text input for better composition support on some browsers.
tarea.Set("rows", 1)
style.Set("resize", "none")
style.Set("overflow", "hidden")
style.Set("white-space", "pre-wrap")
style.Set("word-break", "normal")
return tarea
}
@@ -188,12 +186,12 @@ func (w *window) addEventListeners() {
w.cnv.Set("width", 0)
w.cnv.Set("height", 0)
w.resize()
w.requestRedraw()
w.draw(true)
return nil
})
w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
w.resize()
w.requestRedraw()
w.draw(true)
return nil
})
w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
@@ -201,22 +199,11 @@ func (w *window) addEventListeners() {
return nil
})
w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
if w.w.Event(key.Event{Name: key.NameBack}) {
if w.processEvent(key.Event{Name: key.NameBack}) {
return w.browserHistory.Call("forward")
}
return w.browserHistory.Call("back")
})
w.addEventListener(w.document, "visibilitychange", func(this js.Value, args []js.Value) interface{} {
ev := system.StageEvent{}
switch w.document.Get("visibilityState").String() {
case "hidden", "prerender", "unloaded":
ev.Stage = system.StagePaused
default:
ev.Stage = system.StageRunning
}
w.w.Event(ev)
return nil
})
w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
w.pointerEvent(pointer.Move, 0, 0, args[0])
return nil
@@ -274,18 +261,28 @@ func (w *window) addEventListeners() {
w.touches[i] = js.Null()
}
w.touches = w.touches[:0]
w.w.Event(pointer.Event{
Type: pointer.Cancel,
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.w.Event(key.FocusEvent{Focus: true})
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.w.Event(key.FocusEvent{Focus: false})
if w.composing != -1 {
// If we're composing, try to cancel.
// On Javascript is not possible to cancel the composition once started.
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
w.composing = -1
}
w.config.Focused = false
w.lastCursor = 0 // Reset cursor tracking on blur
w.processEvent(ConfigEvent{Config: w.config})
w.blur()
return nil
})
@@ -298,19 +295,205 @@ func (w *window) addEventListeners() {
return nil
})
w.addEventListener(w.tarea, "compositionstart", func(this js.Value, args []js.Value) interface{} {
w.composing = true
st := w.w.EditorState()
sel := st.Selection.Range
if sel.Start == -1 {
sel.Start = 0
sel.End = 0
}
w.w.SetEditorSnippet(key.Range{Start: sel.Start, End: sel.End})
w.composing = sel.Start
return nil
})
w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} {
w.composing = false
w.flushInput()
finalText := w.tarea.Get("value").String()
if w.composing != -1 && finalText != "" {
// Replace the entire composition range with the final text.
compEnd := w.composing + utf8.RuneCountInString(finalText)
replaceRange := key.Range{Start: w.composing, End: compEnd}
w.w.EditorReplace(replaceRange, finalText)
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
// Position cursor after the final composition text.
newEnd := w.composing + utf8.RuneCountInString(finalText)
w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd})
}
w.composing = -1
w.tarea.Set("value", "")
return nil
})
w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} {
if w.composing {
return nil
e := args[0]
inputType := e.Get("inputType").String()
dataVal := e.Get("data")
var data string
if dataVal.Truthy() {
data = dataVal.String()
}
w.flushInput()
// Get the current textarea value.
tareaValue := w.tarea.Get("value").String()
st := w.w.EditorState()
sel := st.Selection.Range
var absStart, absEnd int
snippetStart := st.Snippet.Range.Start
snippetEnd := st.Snippet.Range.End
cursorPos := sel.Start
selectionEnd := sel.End
if cursorPos < 0 {
cursorPos = 0
selectionEnd = 0
}
// Check if we need to expand the snippet to include the range.
if st.Snippet.Range.Start == 0 && st.Snippet.Range.End == 0 && tareaValue != "" {
// Empty snippet - set it to include the selection/cursor.
w.w.SetEditorSnippet(key.Range{Start: cursorPos, End: selectionEnd})
absStart = cursorPos
absEnd = selectionEnd
} else if cursorPos < snippetStart || selectionEnd > snippetEnd {
// Selection is outside the snippet
newStart := snippetStart
newEnd := snippetEnd
if cursorPos < newStart {
newStart = cursorPos
}
if selectionEnd > newEnd {
newEnd = selectionEnd
}
w.w.SetEditorSnippet(key.Range{Start: newStart, End: newEnd})
// Refresh state after snippet update.
st = w.w.EditorState()
// Use the selection range directly.
absStart = cursorPos
absEnd = selectionEnd
} else {
// Selection is within snippet to absolute positions.
absStart = cursorPos
absEnd = selectionEnd
}
switch inputType {
case "insertCompositionText":
if w.composing == -1 {
break
}
compEnd := absEnd
if compEnd < w.composing {
compEnd = w.composing
}
replaceRange := key.Range{Start: w.composing, End: compEnd}
w.w.EditorReplace(replaceRange, data)
newEnd := w.composing + utf8.RuneCountInString(data)
w.w.SetComposingRegion(key.Range{Start: w.composing, End: newEnd})
w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd})
case "deleteContentBackward", "deleteContentForward", "deleteByCut":
if w.composing != -1 {
compEnd := w.composing + utf8.RuneCountInString(tareaValue)
replaceRange := key.Range{Start: w.composing, End: compEnd}
w.w.EditorReplace(replaceRange, tareaValue)
newEnd := w.composing + utf8.RuneCountInString(tareaValue)
w.w.SetComposingRegion(key.Range{Start: w.composing, End: newEnd})
w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd})
} else {
replaceRange := key.Range{Start: absStart, End: absEnd}
w.w.EditorReplace(replaceRange, "")
w.w.SetEditorSelection(key.Range{Start: absStart, End: absStart})
}
case "insertReplacementText":
if w.composing != -1 {
// During composition, replace the entire composition.
compEnd := w.composing + utf8.RuneCountInString(data)
replaceRange := key.Range{Start: w.composing, End: compEnd}
w.w.EditorReplace(replaceRange, data)
newEnd := w.composing + utf8.RuneCountInString(data)
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd})
w.composing = -1
w.lastCursor = newEnd
} else {
// Safari sends "insertReplacementText" for autocorrect, but the cursor is at the end of the word, so we need to find the word start.
insertLen := utf8.RuneCountInString(data)
wordStart := absStart
if absStart > snippetStart {
relPos := absStart - snippetStart
snippetRunes := []rune(st.Snippet.Text)
for i := relPos - 1; i >= 0; i-- {
if i >= len(snippetRunes) {
continue
}
r := snippetRunes[i]
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
break
}
wordStart = snippetStart + i
}
}
replaceRange := key.Range{Start: wordStart, End: absStart}
w.w.EditorReplace(replaceRange, data)
newCursor := wordStart + insertLen
w.w.SetEditorSelection(key.Range{Start: newCursor, End: newCursor})
w.lastCursor = newCursor
}
case "insertText":
if w.composing != -1 {
compEnd := w.composing + utf8.RuneCountInString(data)
replaceRange := key.Range{Start: w.composing, End: compEnd}
w.w.EditorReplace(replaceRange, data)
newEnd := w.composing + utf8.RuneCountInString(data)
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd})
w.composing = -1
w.lastCursor = newEnd
} else {
insertLen := utf8.RuneCountInString(data)
replaceRange := key.Range{Start: absStart, End: absStart}
if absStart != absEnd {
replaceRange = key.Range{Start: absStart, End: absEnd}
}
newCursor := replaceRange.Start + insertLen
w.w.EditorReplace(replaceRange, data)
w.w.SetEditorSelection(key.Range{Start: newCursor, End: newCursor})
w.lastCursor = newCursor
}
default: // paste and other input types
if w.composing != -1 {
compEnd := w.composing + utf8.RuneCountInString(tareaValue)
replaceRange := key.Range{Start: w.composing, End: compEnd}
w.w.EditorReplace(replaceRange, tareaValue)
newEnd := w.composing + utf8.RuneCountInString(tareaValue)
w.w.SetComposingRegion(key.Range{Start: w.composing, End: newEnd})
w.w.SetEditorSelection(key.Range{Start: newEnd, End: newEnd})
} else {
replaceRange := key.Range{Start: absStart, End: absEnd}
w.w.EditorReplace(replaceRange, tareaValue)
newCursor := absStart + utf8.RuneCountInString(tareaValue)
w.w.SetEditorSelection(key.Range{Start: newCursor, End: newCursor})
}
}
return nil
})
w.addEventListener(w.tarea, "paste", func(this js.Value, args []js.Value) interface{} {
@@ -327,12 +510,6 @@ 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
@@ -364,20 +541,98 @@ func (w *window) keyboard(hint key.InputHint) {
m = "text"
}
w.tarea.Set("inputMode", m)
// Update autocomplete / autocorrect attributes.
var autocomplete, autocorrect, autocapitalize string
var spellcheck bool
switch hint {
case key.HintAny, key.HintText:
autocomplete, autocorrect, autocapitalize, spellcheck = "on", "on", "on", true
case key.HintEmail:
autocomplete, autocorrect, autocapitalize, spellcheck = "email", "off", "off", false
case key.HintURL:
autocomplete, autocorrect, autocapitalize, spellcheck = "url", "off", "off", false
case key.HintTelephone:
autocomplete, autocorrect, autocapitalize, spellcheck = "tel", "off", "off", false
case key.HintPassword:
autocomplete, autocorrect, autocapitalize, spellcheck = "current-password", "off", "off", false
default: // key.HintNumeric and others
autocomplete, autocorrect, autocapitalize, spellcheck = "off", "off", "off", false
}
w.tarea.Set("autocomplete", autocomplete)
w.tarea.Set("autocorrect", autocorrect)
w.tarea.Set("autocapitalize", autocapitalize)
w.tarea.Set("spellcheck", spellcheck)
}
func (w *window) keyEvent(e js.Value, ks key.State) {
k := e.Get("key").String()
if n, ok := translateKey(k); ok {
if ks == key.Press {
isMod := n == key.NameAlt || n == key.NameCommand || n == key.NameCtrl || n == key.NameShift || n == key.NameSuper
isFunc := n == key.NameUpArrow || n == key.NameDownArrow || n == key.NameLeftArrow || n == key.NameRightArrow ||
n == key.NamePageUp || n == key.NamePageDown || n == key.NameHome || n == key.NameEnd ||
n == key.NameEscape || n == key.NameReturn || n == key.NameEnter || n == key.NameTab ||
n == key.NameDeleteBackward || n == key.NameDeleteForward
if isMod || isFunc {
// Gio will request the browser to change the selection/carret position natively.
e.Call("preventDefault")
}
}
cmd := key.Event{
Name: n,
Modifiers: modifiersFor(e),
State: ks,
}
w.w.Event(cmd)
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 {
@@ -395,10 +650,16 @@ func modifiersFor(e js.Value) key.Modifiers {
if e.Call("getModifierState", "Shift").Bool() {
mods |= key.ModShift
}
if e.Call("getModifierState", "Meta").Bool() {
mods |= key.ModCommand
}
if e.Call("getModifierState", "OS").Bool() {
mods |= key.ModSuper
}
return mods
}
func (w *window) touchEvent(typ pointer.Type, e js.Value) {
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")
@@ -415,6 +676,9 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) {
if e.Get("ctrlKey").Bool() {
mods |= key.ModCtrl
}
if e.Get("metaKey").Bool() {
mods |= key.ModCommand
}
for i := 0; i < n; i++ {
touch := changedTouches.Index(i)
pid := w.touchIDFor(touch)
@@ -425,8 +689,8 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) {
X: float32(x) * scale,
Y: float32(y) * scale,
}
w.w.Event(pointer.Event{
Type: typ,
w.processEvent(pointer.Event{
Kind: kind,
Source: pointer.Touch,
Position: pos,
PointerID: pid,
@@ -448,7 +712,7 @@ func (w *window) touchIDFor(touch js.Value) pointer.ID {
return pid
}
func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
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")
@@ -475,8 +739,8 @@ func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
if jbtns&4 != 0 {
btns |= pointer.ButtonTertiary
}
w.w.Event(pointer.Event{
Type: typ,
w.processEvent(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Buttons: btns,
Position: pos,
@@ -502,19 +766,91 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
return jsf
}
func (w *window) animCallback() {
anim := w.animating
w.animRequested = anim
if anim {
w.requestAnimationFrame.Invoke(w.redraw)
func (w *window) EditorStateChanged(old, new editorState) {
if w.composing != -1 {
// Do not interfere with browser state while composing.
// On Javascript is not possible to cancel the composition once started!
return
}
if anim {
w.draw(false)
// Update textarea value to match the snippet.
if old.Snippet != new.Snippet {
w.tarea.Set("value", new.Snippet.Text)
}
// Update selection to match Gio's selection.
if old.Selection.Range != new.Selection.Range || old.Snippet != new.Snippet {
if new.Selection.Range.Start != -1 && new.Selection.Range.End != -1 {
// Calculate selection positions relative to snippet start.
// The textarea contains only the snippet text.
snippetStart := new.Snippet.Range.Start
snippetEnd := new.Snippet.Range.End
selStart := new.Selection.Range.Start
selEnd := new.Selection.Range.End
if selStart < snippetStart {
selStart = snippetStart
}
if selStart > snippetEnd {
selStart = snippetEnd
}
if selEnd < snippetStart {
selEnd = snippetStart
}
if selEnd > snippetEnd {
selEnd = snippetEnd
}
// Convert absolute rune positions to UTF-16 positions for the textarea.
startUTF16 := new.UTF16Index(selStart)
endUTF16 := new.UTF16Index(selEnd)
// Convert to snippet-relative UTF-16 positions.
snippetStartUTF16 := new.UTF16Index(snippetStart)
start := startUTF16 - snippetStartUTF16
end := endUTF16 - snippetStartUTF16
if start < 0 {
start = 0
}
if end < 0 {
end = 0
}
// Calculate max UTF-16 length of snippet text.
textLen := new.UTF16Index(snippetEnd) - snippetStartUTF16
if start > textLen {
start = textLen
}
if end > textLen {
end = textLen
}
if start > end {
start, end = end, start
}
w.tarea.Set("selectionStart", start)
w.tarea.Set("selectionEnd", end)
}
}
// Move DOM element to position the caret.
if old.Selection.Caret != new.Selection.Caret || old.Selection.Transform != new.Selection.Transform {
pos := new.Selection.Transform.Transform(new.Selection.Caret.Pos.Add(f32.Pt(0, new.Selection.Caret.Descent)))
bounds := w.cnv.Call("getBoundingClientRect")
left := bounds.Get("left").Float() + float64(pos.X)/float64(w.scale)
top := bounds.Get("top").Float() + float64(pos.Y-new.Selection.Caret.Ascent)/float64(w.scale)
height := float64(new.Selection.Caret.Ascent+new.Selection.Caret.Descent) / float64(w.scale)
style := w.tarea.Get("style")
style.Set("left", fmt.Sprintf("%fpx", left))
style.Set("top", fmt.Sprintf("%fpx", top))
style.Set("height", fmt.Sprintf("%fpx", height))
style.Set("width", "1px")
}
}
func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) SetAnimating(anim bool) {
w.animating = anim
if anim && !w.animRequested {
@@ -527,20 +863,19 @@ func (w *window) ReadClipboard() {
if w.clipboard.IsUndefined() {
return
}
if w.clipboard.Get("readText").IsUndefined() {
return
if w.clipboard.Get("readText").Truthy() {
w.clipboard.Call("readText").Call("then", w.clipboardCallback)
}
w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
}
func (w *window) WriteClipboard(s string) {
func (w *window) WriteClipboard(mime string, s []byte) {
if w.clipboard.IsUndefined() {
return
}
if w.clipboard.Get("writeText").IsUndefined() {
return
}
w.clipboard.Call("writeText", s)
w.clipboard.Call("writeText", string(s))
}
func (w *window) Configure(options []Option) {
@@ -568,7 +903,7 @@ func (w *window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.w.Event(ConfigEvent{Config: w.config})
w.processEvent(ConfigEvent{Config: w.config})
}
func (w *window) Perform(system.Action) {}
@@ -607,23 +942,21 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
style.Set("cursor", webCursor[cursor])
}
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
}
func (w *window) ShowTextInput(show bool) {
// Run in a goroutine to avoid a deadlock if the
// focus change result in an event.
go func() {
if show {
w.focus()
} else {
w.blur()
if show {
w.focus()
} else {
// If we're composing, end composition first by clearing the textarea.
// That is a attempt to force the browser to end composition.
if w.composing != -1 {
w.tarea.Set("value", "")
w.composing = -1
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
}
}()
w.blur()
}
}
func (w *window) SetInputHint(mode key.InputHint) {
@@ -640,7 +973,7 @@ func (w *window) resize() {
}
if size != w.config.Size {
w.config.Size = size
w.w.Event(ConfigEvent{Config: w.config})
w.processEvent(ConfigEvent{Config: w.config})
}
if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
@@ -660,13 +993,20 @@ 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.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: size,
Insets: insets,
@@ -676,10 +1016,10 @@ func (w *window) draw(sync bool) {
})
}
func (w *window) getConfig() (image.Point, system.Insets, unit.Metric) {
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),
system.Insets{
Insets{
Bottom: unit.Dp(w.inset.Y) * invscale,
Right: unit.Dp(w.inset.X) * invscale,
}, unit.Metric{
@@ -735,19 +1075,12 @@ func (w *window) navigationColor(c color.NRGBA) {
theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
}
func (w *window) requestRedraw() {
select {
case w.chanRedraw <- struct{}{}:
default:
}
}
func osMain() {
select {}
}
func translateKey(k string) (string, bool) {
var n string
func translateKey(k string) (key.Name, bool) {
var n key.Name
switch k {
case "ArrowUp":
@@ -814,11 +1147,15 @@ func translateKey(k string) (string, bool) {
r, s := utf8.DecodeRuneInString(k)
// If there is exactly one printable character, return that.
if s == len(k) && unicode.IsPrint(r) {
return strings.ToUpper(k), true
return key.Name(strings.ToUpper(k)), true
}
return "", false
}
return n, true
}
func (_ ViewEvent) ImplementsEvent() {}
func (JSViewEvent) implementsViewEvent() {}
func (JSViewEvent) ImplementsEvent() {}
func (j JSViewEvent) Valid() bool {
return !(j.Element.IsNull() || j.Element.IsUndefined())
}
+521 -323
View File
File diff suppressed because it is too large Load Diff
+124 -66
View File
@@ -6,7 +6,7 @@
#include "_cgo_export.h"
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWithTrans);
@interface GioAppDelegate : NSObject<NSApplicationDelegate>
@end
@@ -14,40 +14,55 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioWindowDelegate : NSObject<NSWindowDelegate>
@end
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@property uintptr_t handle;
@property BOOL presentWithTrans;
@end
@implementation GioWindowDelegate
- (void)windowWillMiniaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onHide((__bridge CFTypeRef)window.contentView);
GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
}
- (void)windowDidDeminiaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onShow((__bridge CFTypeRef)window.contentView);
GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onFullscreen((__bridge CFTypeRef)window.contentView);
GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
}
- (void)windowWillExitFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onWindowed((__bridge CFTypeRef)window.contentView);
GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
}
- (void)windowDidChangeScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
CFTypeRef view = (__bridge CFTypeRef)window.contentView;
gio_onChangeScreen(view, dispID);
GioView *view = (GioView *)window.contentView;
gio_onChangeScreen(view.handle, dispID);
}
- (void)windowDidBecomeKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onFocus((__bridge CFTypeRef)window.contentView, 1);
GioView *view = (GioView *)window.contentView;
if ([window firstResponder] == view) {
gio_onFocus(view.handle, 1);
}
}
- (void)windowDidResignKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onFocus((__bridge CFTypeRef)window.contentView, 0);
GioView *view = (GioView *)window.contentView;
if ([window firstResponder] == view) {
gio_onFocus(view.handle, 0);
}
}
@end
static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
if (!event.hasPreciseScrollingDeltas) {
// dx and dy are in rows and columns.
@@ -56,12 +71,9 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
}
// Origin is in the lower left corner. Convert to upper left.
CGFloat height = view.bounds.size.height;
gio_onMouse((__bridge CFTypeRef)view, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
}
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@end
@implementation GioView
- (void)setFrameSize:(NSSize)newSize {
[super setFrameSize:newSize];
@@ -70,21 +82,19 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
// drawRect is called when OpenGL is used, displayLayer otherwise.
// Don't know why.
- (void)drawRect:(NSRect)r {
gio_onDraw((__bridge CFTypeRef)self);
gio_onDraw(self.handle);
}
- (void)displayLayer:(CALayer *)layer {
layer.contentsScale = self.window.backingScaleFactor;
gio_onDraw((__bridge CFTypeRef)self);
gio_onDraw(self.handle);
}
- (CALayer *)makeBackingLayer {
CALayer *layer = gio_layerFactory();
CALayer *layer = gio_layerFactory(self.presentWithTrans);
layer.delegate = self;
return layer;
}
- (void)viewDidMoveToWindow {
if (self.window == nil) {
gio_onClose((__bridge CFTypeRef)self);
}
gio_onAttached(self.handle, self.window != nil ? 1 : 0);
}
- (void)mouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
@@ -122,34 +132,37 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
handleMouse(self, event, MOUSE_SCROLL, dx, dy);
}
- (void)keyDown:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
gio_onKeys(self.handle, (__bridge CFTypeRef)event, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
}
- (void)flagsChanged:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
gio_onFlagsChanged(self.handle, [event modifierFlags]);
}
- (void)keyUp:(NSEvent *)event {
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
gio_onKeys(self.handle, (__bridge CFTypeRef)event, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
}
- (void)insertText:(id)string {
gio_onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)string);
gio_onText(self.handle, (__bridge CFTypeRef)string);
}
- (void)doCommandBySelector:(SEL)sel {
// Don't pass commands up the responder chain.
// They will end up in a beep.
- (void)doCommandBySelector:(SEL)action {
if (!gio_onCommandBySelector(self.handle)) {
[super doCommandBySelector:action];
}
}
- (BOOL)hasMarkedText {
int res = gio_hasMarkedText((__bridge CFTypeRef)self);
int res = gio_hasMarkedText(self.handle);
return res ? YES : NO;
}
- (NSRange)markedRange {
return gio_markedRange((__bridge CFTypeRef)self);
return gio_markedRange(self.handle);
}
- (NSRange)selectedRange {
return gio_selectedRange((__bridge CFTypeRef)self);
return gio_selectedRange(self.handle);
}
- (void)unmarkText {
gio_unmarkText((__bridge CFTypeRef)self);
gio_unmarkText(self.handle);
}
- (void)setMarkedText:(id)string
selectedRange:(NSRange)selRange
@@ -161,14 +174,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
} else {
str = string;
}
gio_setMarkedText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, selRange, replaceRange);
gio_setMarkedText(self.handle, (__bridge CFTypeRef)str, selRange, replaceRange);
}
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
return nil;
}
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
actualRange:(NSRangePointer)actualRange {
NSString *str = CFBridgingRelease(gio_substringForProposedRange((__bridge CFTypeRef)self, range, actualRange));
NSString *str = CFBridgingRelease(gio_substringForProposedRange(self.handle, range, actualRange));
return [[NSAttributedString alloc] initWithString:str attributes:nil];
}
- (void)insertText:(id)string
@@ -180,17 +193,34 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
} else {
str = string;
}
gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange);
gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange);
}
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
return gio_characterIndexForPoint((__bridge CFTypeRef)self, p);
return gio_characterIndexForPoint(self.handle, p);
}
- (NSRect)firstRectForCharacterRange:(NSRange)rng
actualRange:(NSRangePointer)actual {
NSRect r = gio_firstRectForCharacterRange((__bridge CFTypeRef)self, rng, actual);
NSRect r = gio_firstRectForCharacterRange(self.handle, rng, actual);
r = [self convertRect:r toView:nil];
return [[self window] convertRectToScreen:r];
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onDraw(self.handle);
}
- (void)applicationDidHide:(NSNotification *)notification {
gio_onDraw(self.handle);
}
- (void)dealloc {
gio_onDestroy(self.handle);
}
- (BOOL) becomeFirstResponder {
gio_onFocus(self.handle, 1);
return [super becomeFirstResponder];
}
- (BOOL) resignFirstResponder {
gio_onFocus(self.handle, 0);
return [super resignFirstResponder];
}
@end
// Delegates are weakly referenced from their peers. Nothing
@@ -240,7 +270,7 @@ void gio_showCursor() {
// some cursors are not public, this tries to use a private cursor
// and uses fallback when the use of private cursor fails.
void gio_trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
static void trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
if ([NSCursor respondsToSelector:cursorName]) {
id object = [NSCursor performSelector:cursorName];
if ([object isKindOfClass:[NSCursor class]]) {
@@ -272,7 +302,7 @@ void gio_setCursor(NSUInteger curID) {
break;
case 6: // pointer.CursorAllScroll
// For some reason, using _moveCursor fails on Monterey.
// gio_trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
// trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
[NSCursor.arrowCursor set];
break;
case 7: // pointer.CursorColResize
@@ -282,33 +312,31 @@ void gio_setCursor(NSUInteger curID) {
[NSCursor.resizeUpDownCursor set];
break;
case 9: // pointer.CursorGrab
// [NSCursor.openHandCursor set];
gio_trySetPrivateCursor(@selector(openHandCursor), NSCursor.arrowCursor);
[NSCursor.openHandCursor set];
break;
case 10: // pointer.CursorGrabbing
// [NSCursor.closedHandCursor set];
gio_trySetPrivateCursor(@selector(closedHandCursor), NSCursor.arrowCursor);
[NSCursor.closedHandCursor set];
break;
case 11: // pointer.CursorNotAllowed
[NSCursor.operationNotAllowedCursor set];
break;
case 12: // pointer.CursorWait
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break;
case 13: // pointer.CursorProgress
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break;
case 14: // pointer.CursorNorthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 15: // pointer.CursorNorthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
break;
case 16: // pointer.CursorSouthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 17: // pointer.CursorSouthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
break;
case 18: // pointer.CursorNorthSouthResize
[NSCursor.resizeUpDownCursor set];
@@ -329,10 +357,10 @@ void gio_setCursor(NSUInteger curID) {
[NSCursor.resizeDownCursor set];
break;
case 24: // pointer.CursorNorthEastSouthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 25: // pointer.CursorNorthWestSouthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
break;
default:
[NSCursor.arrowCursor set];
@@ -341,7 +369,7 @@ void gio_setCursor(NSUInteger curID) {
}
}
CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight) {
CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height) {
@autoreleasepool {
NSRect rect = NSMakeRect(0, 0, width, height);
NSUInteger styleMask = NSTitledWindowMask |
@@ -353,42 +381,50 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:NO];
if (minWidth > 0 || minHeight > 0) {
window.contentMinSize = NSMakeSize(minWidth, minHeight);
}
if (maxWidth > 0 || maxHeight > 0) {
window.contentMaxSize = NSMakeSize(maxWidth, maxHeight);
}
[window setAcceptsMouseMovedEvents:YES];
NSView *view = (__bridge NSView *)viewRef;
[window setContentView:view];
[window makeFirstResponder:view];
window.delegate = globalWindowDel;
return (__bridge_retained CFTypeRef)window;
}
}
CFTypeRef gio_createView(void) {
CFTypeRef gio_createView(int presentWithTrans) {
@autoreleasepool {
NSRect frame = NSMakeRect(0, 0, 0, 0);
GioView* view = [[GioView alloc] initWithFrame:frame];
view.presentWithTrans = presentWithTrans ? YES : NO;
view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
[[NSNotificationCenter defaultCenter] addObserver:view
selector:@selector(applicationWillUnhide:)
name:NSApplicationWillUnhideNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:view
selector:@selector(applicationDidHide:)
name:NSApplicationDidHideNotification
object:nil];
return CFBridgingRetain(view);
}
}
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
@autoreleasepool {
GioView *v = (__bridge GioView *)viewRef;
v.handle = handle;
}
}
@implementation GioAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES];
gio_onFinishLaunching();
}
- (void)applicationDidHide:(NSNotification *)aNotification {
gio_onAppHide();
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onAppShow();
- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
for (NSURL *url in urls) {
gio_onOpenURI((__bridge CFTypeRef)url.absoluteString);
}
}
@end
@@ -419,3 +455,25 @@ void gio_main() {
[NSApp run];
}
}
@interface AppListener : NSObject
@end
static AppListener *appListener;
@implementation AppListener
- (void)launchFinished:(NSNotification *)notification {
appListener = nil;
gio_onFinishLaunching();
}
@end
void gio_init() {
@autoreleasepool {
appListener = [[AppListener alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:appListener
selector:@selector(launchFinished:)
name:NSApplicationDidFinishLaunchingNotification
object:nil];
}
}
+11 -13
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build (linux && !android) || freebsd || openbsd
// +build linux,!android freebsd openbsd
package app
@@ -12,13 +11,6 @@ import (
"gioui.org/io/pointer"
)
// ViewEvent provides handles to the underlying window objects for the
// current display protocol.
type ViewEvent interface {
implementsViewEvent()
ImplementsEvent()
}
type X11ViewEvent struct {
// Display is a pointer to the X11 Display created by XOpenDisplay.
Display unsafe.Pointer
@@ -28,6 +20,9 @@ type X11ViewEvent struct {
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.
@@ -38,6 +33,9 @@ type WaylandViewEvent struct {
func (WaylandViewEvent) implementsViewEvent() {}
func (WaylandViewEvent) ImplementsEvent() {}
func (w WaylandViewEvent) Valid() bool {
return w != (WaylandViewEvent{})
}
func osMain() {
select {}
@@ -49,7 +47,7 @@ type windowDriver func(*callbacks, []Option) error
// let each driver initialize these variables with their own version of createWindow.
var wlDriver, x11Driver windowDriver
func newWindow(window *callbacks, options []Option) error {
func newWindow(window *callbacks, options []Option) {
var errFirst error
for _, d := range []windowDriver{wlDriver, x11Driver} {
if d == nil {
@@ -57,16 +55,16 @@ func newWindow(window *callbacks, options []Option) error {
}
err := d(window, options)
if err == nil {
return nil
return
}
if errFirst == nil {
errFirst = err
}
}
if errFirst != nil {
return errFirst
if errFirst == nil {
errFirst = errors.New("app: no window driver available")
}
return errors.New("app: no window driver available")
window.ProcessEvent(DestroyEvent{Err: errFirst})
}
// xCursor contains mapping from pointer.Cursor to XCursor.
+215 -154
View File
@@ -15,6 +15,7 @@ import (
"math"
"os"
"os/exec"
"runtime"
"strconv"
"sync"
"time"
@@ -25,10 +26,12 @@ import (
"gioui.org/app/internal/xkb"
"gioui.org/f32"
"gioui.org/internal/fling"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
)
@@ -94,13 +97,12 @@ type wlDisplay struct {
// Notification pipe fds.
notify struct {
read int
mu sync.Mutex
write int
read, write int
}
repeat repeatState
repeat repeatState
poller poller
readClipClose chan struct{}
}
type wlSeat struct {
@@ -114,6 +116,8 @@ type wlSeat struct {
// The most recent input serial.
serial C.uint32_t
// The most recent pointer enter serial.
pointerSerial C.uint32_t
pointerFocus *window
keyboardFocus *window
@@ -140,7 +144,7 @@ type repeatState struct {
delay time.Duration
key uint32
win *callbacks
win *window
stopC chan struct{}
start time.Duration
@@ -152,7 +156,6 @@ type repeatState struct {
type window struct {
w *callbacks
disp *wlDisplay
seat *wlSeat
surf *C.struct_wl_surface
wmSurf *C.struct_xdg_surface
topLvl *C.struct_xdg_toplevel
@@ -197,12 +200,10 @@ type window struct {
dir f32.Point
}
stage system.Stage
dead bool
configured bool
lastFrameCallback *C.struct_wl_callback
animating bool
redraw bool
// The most recent configure serial waiting to be ack'ed.
serial C.uint32_t
scale int
@@ -212,9 +213,11 @@ type window struct {
wsize image.Point // window config size before going fullscreen or maximized
inCompositor bool // window is moving or being resized
clipReads chan clipboard.Event
clipReads chan transfer.DataEvent
wakeups chan struct{}
closing bool
}
type poller struct {
@@ -263,25 +266,17 @@ func newWLWindow(callbacks *callbacks, options []Option) error {
return err
}
w.w = callbacks
go func() {
defer d.destroy()
defer w.destroy()
w.w.SetDriver(w)
w.w.SetDriver(w)
// Finish and commit setup from createNativeWindow.
w.Configure(options)
w.draw(true)
C.wl_surface_commit(w.surf)
// Finish and commit setup from createNativeWindow.
w.Configure(options)
C.wl_surface_commit(w.surf)
w.w.Event(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
err := w.loop()
w.w.Event(WaylandViewEvent{})
w.w.Event(system.DestroyEvent{Err: err})
}()
w.ProcessEvent(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
return nil
}
@@ -357,7 +352,7 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) {
ppdp: ppdp,
ppsp: ppdp,
wakeups: make(chan struct{}, 1),
clipReads: make(chan clipboard.Event, 1),
clipReads: make(chan transfer.DataEvent, 1),
}
w.surf = C.wl_compositor_create_surface(d.compositor)
if w.surf == nil {
@@ -552,15 +547,15 @@ func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) {
func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
w := callbackLoad(data).(*window)
w.serial = serial
w.redraw = true
C.xdg_surface_ack_configure(wmSurf, serial)
w.setStage(system.StageRunning)
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.dead = true
w.closing = true
}
//export gio_onToplevelConfigure
@@ -589,8 +584,8 @@ func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_
} else {
w.size.Y += int(w.config.decoHeight)
}
w.w.Event(ConfigEvent{Config: w.config})
w.redraw = true
w.ProcessEvent(ConfigEvent{Config: w.config})
w.draw(true)
}
}
@@ -648,7 +643,7 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *
if w.config.Mode == Minimized {
// Minimized window got brought back up: it is no longer so.
w.config.Mode = Windowed
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
}
@@ -793,8 +788,8 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
Type: pointer.Press,
w.ProcessEvent(pointer.Event{
Kind: pointer.Press,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
@@ -809,8 +804,8 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui
s.serial = serial
w := s.touchFoci[id]
delete(s.touchFoci, id)
w.w.Event(pointer.Event{
Type: pointer.Release,
w.ProcessEvent(pointer.Event{
Kind: pointer.Release,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
@@ -827,8 +822,8 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
Type: pointer.Move,
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Position: w.lastTouch,
Source: pointer.Touch,
PointerID: pointer.ID(id),
@@ -846,8 +841,8 @@ 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.w.Event(pointer.Event{
Type: pointer.Cancel,
w.ProcessEvent(pointer.Event{
Kind: pointer.Cancel,
Source: pointer.Touch,
})
}
@@ -857,8 +852,8 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
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)
w.seat = s
s.pointerFocus = w
w.setCursor(pointer, serial)
w.lastPos = f32.Point{X: fromFixed(x), Y: fromFixed(y)}
@@ -867,12 +862,12 @@ func gio_onPointerEnter(data unsafe.Pointer, pointer *C.struct_wl_pointer, seria
//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)
w.seat = nil
s := callbackLoad(data).(*wlSeat)
s.serial = serial
s.pointerFocus = nil
if w.inCompositor {
w.inCompositor = false
w.w.Event(pointer.Event{Type: pointer.Cancel})
w.ProcessEvent(pointer.Event{Kind: pointer.Cancel})
}
}
@@ -889,11 +884,13 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
s := callbackLoad(data).(*wlSeat)
s.serial = serial
w := s.pointerFocus
// From linux-event-codes.h.
// 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 {
@@ -903,6 +900,10 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
btn = pointer.ButtonSecondary
case BTN_MIDDLE:
btn = pointer.ButtonTertiary
case BTN_SIDE:
btn = pointer.ButtonQuaternary
case BTN_EXTRA:
btn = pointer.ButtonQuinary
default:
return
}
@@ -920,21 +921,21 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
}
}
}
var typ pointer.Type
var kind pointer.Kind
switch state {
case 0:
w.pointerBtns &^= btn
typ = pointer.Release
kind = pointer.Release
// Move or resize gestures no longer applies.
w.inCompositor = false
case 1:
w.pointerBtns |= btn
typ = pointer.Press
kind = pointer.Press
}
w.flushScroll()
w.resetFling()
w.w.Event(pointer.Event{
Type: typ,
w.ProcessEvent(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: w.lastPos,
@@ -969,6 +970,9 @@ func gio_onPointerAxis(data unsafe.Pointer, p *C.struct_wl_pointer, t, axis C.ui
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()
}
@@ -1021,23 +1025,34 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis
}
func (w *window) ReadClipboard() {
if w.disp.readClipClose != nil {
return
}
w.disp.readClipClose = make(chan struct{})
r, err := w.disp.readClipboard()
// Send empty responses on unavailable clipboards or errors.
if r == nil || err != nil {
w.w.Event(clipboard.Event{})
return
}
// Don't let slow clipboard transfers block event loop.
go func() {
defer r.Close()
data, _ := io.ReadAll(r)
w.clipReads <- clipboard.Event{Text: string(data)}
w.Wakeup()
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(s string) {
w.disp.writeClipboard([]byte(s))
func (w *window) WriteClipboard(mime string, s []byte) {
w.disp.writeClipboard(s)
}
func (w *window) Configure(options []Option) {
@@ -1095,8 +1110,7 @@ func (w *window) Configure(options []Option) {
w.config.MaxSize = cnf.MaxSize
w.setWindowConstraints()
}
w.w.Event(ConfigEvent{Config: w.config})
w.redraw = true
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *window) setWindowConstraints() {
@@ -1133,22 +1147,23 @@ func (w *window) Perform(actions system.Action) {
walkActions(actions, func(action system.Action) {
switch action {
case system.ActionClose:
w.dead = true
w.closing = true
}
})
}
func (w *window) move(serial C.uint32_t) {
s := w.seat
if !w.inCompositor && s != nil {
w.inCompositor = true
C.xdg_toplevel_move(w.topLvl, s.seat, serial)
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.seat
if w.inCompositor || s == nil {
s := w.disp.seat
if w.inCompositor || s.pointerFocus != w {
return
}
w.inCompositor = true
@@ -1161,11 +1176,12 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
}
func (w *window) updateCursor() {
ptr := w.disp.seat.pointer
if ptr == nil {
s := w.disp.seat
ptr := s.pointer
if ptr == nil || s.pointerFocus != w {
return
}
w.setCursor(ptr, w.serial)
w.setCursor(ptr, s.pointerSerial)
}
func (w *window) setCursor(pointer *C.struct_wl_pointer, serial C.uint32_t) {
@@ -1174,7 +1190,7 @@ func (w *window) setCursor(pointer *C.struct_wl_pointer, serial C.uint32_t) {
c = w.cursor.cursor
}
if c == nil {
C.wl_pointer_set_cursor(pointer, w.serial, nil, 0, 0)
C.wl_pointer_set_cursor(pointer, serial, nil, 0, 0)
return
}
// Get images[0].
@@ -1216,7 +1232,8 @@ func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
w := callbackLoad(unsafe.Pointer(surf)).(*window)
s.keyboardFocus = w
s.disp.repeat.Stop(0)
w.w.Event(key.FocusEvent{Focus: true})
w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onKeyboardLeave
@@ -1225,7 +1242,8 @@ func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
s.serial = serial
s.disp.repeat.Stop(0)
w := s.keyboardFocus
w.w.Event(key.FocusEvent{Focus: false})
w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onKeyboardKey
@@ -1243,7 +1261,7 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.w.Event(e)
w.ProcessEvent(e)
}
}
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
@@ -1278,7 +1296,7 @@ func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
r.now = 0
r.stopC = stopC
r.key = keyCode
r.win = w.w
r.win = w
rate, delay := r.rate, r.delay
go func() {
timer := time.NewTimer(delay)
@@ -1336,9 +1354,9 @@ func (r *repeatState) Repeat(d *wlDisplay) {
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.EditorInsert(ee.Text)
r.win.w.EditorInsert(ee.Text)
} else {
r.win.Event(e)
r.win.ProcessEvent(e)
}
}
r.last += delay
@@ -1351,28 +1369,68 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
w := callbackLoad(data).(*window)
if w.lastFrameCallback == callback {
w.lastFrameCallback = nil
w.draw(false)
}
}
func (w *window) loop() error {
var p poller
for {
if err := w.disp.dispatch(&p); err != nil {
return err
}
select {
case e := <-w.clipReads:
w.w.Event(e)
case <-w.wakeups:
w.w.Event(wakeupEvent{})
default:
}
if w.dead {
break
}
w.draw()
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
}
return nil
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
@@ -1388,13 +1446,21 @@ func (d *wlDisplay) bindDataDevice() {
}
}
func (d *wlDisplay) dispatch(p *poller) error {
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(p.pollfds[:0],
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 {
@@ -1405,11 +1471,25 @@ func (d *wlDisplay) dispatch(p *poller) error {
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, p.buf[:])
_, err := syscall.Read(d.notify.read, d.poller.buf[:])
if err == syscall.EAGAIN {
break
}
@@ -1417,45 +1497,30 @@ func (d *wlDisplay) dispatch(p *poller) error {
return fmt.Errorf("wayland: read from notify pipe failed: %v", err)
}
}
// Handle events
switch {
case dispFd.Revents&syscall.POLLIN != 0:
if ret, err := C.wl_display_dispatch(d.disp); ret < 0 {
return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err)
}
case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0:
return errors.New("wayland: display file descriptor gone")
}
d.repeat.Repeat(d)
return nil
}
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
w.disp.wakeup()
}
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)
d.notify.mu.Lock()
defer d.notify.mu.Unlock()
if d.notify.write == 0 {
return
}
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)
}
@@ -1580,8 +1645,16 @@ func (w *window) flushScroll() {
if total == (f32.Point{}) {
return
}
w.w.Event(pointer.Event{
Type: pointer.Scroll,
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,
@@ -1589,12 +1662,6 @@ func (w *window) flushScroll() {
Time: w.scroll.time,
Modifiers: w.disp.xkb.Modifiers(),
})
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)
}
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
}
func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
@@ -1603,8 +1670,8 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
Type: pointer.Move,
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Position: w.lastPos,
Buttons: w.pointerBtns,
Source: pointer.Mouse,
@@ -1624,7 +1691,8 @@ func (w *window) systemGesture() (*C.struct_wl_cursor, C.uint32_t) {
if w.config.Mode != Windowed || w.config.Decorated {
return nil, 0
}
border := w.w.w.metric.Dp(3)
_, 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
@@ -1678,13 +1746,10 @@ func (w *window) updateOutputs() {
if found && scale != w.scale {
w.scale = scale
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
w.redraw = true
w.draw(true)
}
if !found {
w.setStage(system.StagePaused)
} else {
w.setStage(system.StageRunning)
w.redraw = true
if found {
w.draw(true)
}
}
@@ -1696,7 +1761,10 @@ func (w *window) getConfig() (image.Point, unit.Metric) {
}
}
func (w *window) draw() {
func (w *window) draw(sync bool) {
if !w.configured {
return
}
w.flushScroll()
size, cfg := w.getConfig()
if cfg == (unit.Metric{}) {
@@ -1704,11 +1772,9 @@ func (w *window) draw() {
}
if size != w.config.Size {
w.config.Size = size
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
anim := w.animating || w.fling.anim.Active()
sync := w.redraw
w.redraw = false
// Draw animation only when not waiting for frame callback.
redrawAnim := anim && w.lastFrameCallback == nil
if !redrawAnim && !sync {
@@ -1719,8 +1785,8 @@ func (w *window) draw() {
// 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.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: cfg,
@@ -1729,14 +1795,6 @@ func (w *window) draw() {
})
}
func (w *window) setStage(s system.Stage) {
if s == w.stage {
return
}
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
}
func (w *window) display() *C.struct_wl_display {
return w.disp.disp
}
@@ -1828,12 +1886,14 @@ func newWLDisplay() (*wlDisplay, error) {
}
func (d *wlDisplay) destroy() {
d.notify.mu.Lock()
if d.readClipClose != nil {
close(d.readClipClose)
d.readClipClose = nil
}
if d.notify.write != 0 {
syscall.Close(d.notify.write)
d.notify.write = 0
}
d.notify.mu.Unlock()
if d.notify.read != 0 {
syscall.Close(d.notify.read)
d.notify.read = 0
@@ -1871,6 +1931,7 @@ func (d *wlDisplay) destroy() {
if d.disp != nil {
C.wl_display_disconnect(d.disp)
callbackDelete(unsafe.Pointer(d.disp))
d.disp = nil
}
}
+517 -200
View File
File diff suppressed because it is too large Load Diff
+124 -93
View File
@@ -26,20 +26,25 @@ package app
*/
import "C"
import (
"errors"
"fmt"
"image"
"io"
"strconv"
"strings"
"sync"
"time"
"unsafe"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
syscall "golang.org/x/sys/unix"
@@ -91,12 +96,10 @@ type x11Window struct {
// _NET_WM_STATE_MAXIMIZED_VERT
wmStateMaximizedVert C.Atom
}
stage system.Stage
metric unit.Metric
notify struct {
read, write int
}
dead bool
animating bool
@@ -109,6 +112,8 @@ type x11Window struct {
config Config
wakeups chan struct{}
handler x11EventHandler
buf [100]byte
}
var (
@@ -151,8 +156,8 @@ func (w *x11Window) ReadClipboard() {
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
}
func (w *x11Window) WriteClipboard(s string) {
w.clipboard.content = []byte(s)
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)
}
@@ -232,7 +237,7 @@ func (w *x11Window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *x11Window) setTitle(prev, cnf Config) {
@@ -375,7 +380,36 @@ func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) {
var x11OneByte = make([]byte, 1)
func (w *x11Window) Wakeup() {
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:
@@ -393,16 +427,20 @@ func (w *x11Window) window() (C.Window, int, int) {
return w.xw, w.config.Size.X, w.config.Size.Y
}
func (w *x11Window) setStage(s system.Stage) {
if s == w.stage {
func (w *x11Window) dispatch() {
if w.x == nil {
// Only Invalidate can wake us up.
<-w.wakeups
w.w.Invalidate()
return
}
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
}
func (w *x11Window) loop() {
h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
select {
case <-w.wakeups:
w.w.Invalidate()
default:
}
xfd := C.XConnectionNumber(w.x)
// Poll for events and notifications.
@@ -412,61 +450,55 @@ func (w *x11Window) loop() {
}
xEvents := &pollfds[0].Revents
// Plenty of room for a backlog of notifications.
buf := make([]byte, 100)
loop:
for !w.dead {
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.
if syn = h.handleEvents(); !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 = h.handleEvents()
if w.dead {
break loop
}
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
break loop
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, buf)
if err == syscall.EAGAIN {
break
}
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
}
}
// Clear notifications.
for {
_, err := syscall.Read(w.notify.read, w.buf[:])
if err == syscall.EAGAIN {
break
}
select {
case <-w.wakeups:
w.w.Event(wakeupEvent{})
default:
}
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: w.metric,
},
Sync: syn,
})
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() {
@@ -484,6 +516,7 @@ func (w *x11Window) destroy() {
}
C.XDestroyWindow(w.x, w.xw)
C.XCloseDisplay(w.x)
w.x = nil
}
// atom is a wrapper around XInternAtom. Callers should cache the result
@@ -541,13 +574,13 @@ func (h *x11EventHandler) handleEvents() bool {
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.w.Event(e)
w.ProcessEvent(e)
}
}
case C.ButtonPress, C.ButtonRelease:
bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
ev := pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Source: pointer.Mouse,
Position: f32.Point{
X: float32(bevt.x),
@@ -557,7 +590,7 @@ func (h *x11EventHandler) handleEvents() bool {
Modifiers: w.xkb.Modifiers(),
}
if bevt._type == C.ButtonRelease {
ev.Type = pointer.Release
ev.Kind = pointer.Release
}
var btn pointer.Buttons
const scrollScale = 10
@@ -569,7 +602,7 @@ func (h *x11EventHandler) handleEvents() bool {
case C.Button3:
btn = pointer.ButtonSecondary
case C.Button4:
ev.Type = pointer.Scroll
ev.Kind = pointer.Scroll
// scroll up or left (if shift is pressed).
if ev.Modifiers == key.ModShift {
ev.Scroll.X = -scrollScale
@@ -578,7 +611,7 @@ func (h *x11EventHandler) handleEvents() bool {
}
case C.Button5:
// scroll down or right (if shift is pressed).
ev.Type = pointer.Scroll
ev.Kind = pointer.Scroll
if ev.Modifiers == key.ModShift {
ev.Scroll.X = +scrollScale
} else {
@@ -587,11 +620,11 @@ func (h *x11EventHandler) handleEvents() bool {
case 6:
// http://xahlee.info/linux/linux_x11_mouse_button_number.html
// scroll left.
ev.Type = pointer.Scroll
ev.Kind = pointer.Scroll
ev.Scroll.X = -scrollScale * 2
case 7:
// scroll right
ev.Type = pointer.Scroll
ev.Kind = pointer.Scroll
ev.Scroll.X = +scrollScale * 2
default:
continue
@@ -603,11 +636,11 @@ func (h *x11EventHandler) handleEvents() bool {
w.pointerBtns &^= btn
}
ev.Buttons = w.pointerBtns
w.w.Event(ev)
w.ProcessEvent(ev)
case C.MotionNotify:
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
w.w.Event(pointer.Event{
Type: pointer.Move,
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: f32.Point{
@@ -621,14 +654,16 @@ func (h *x11EventHandler) handleEvents() bool {
// redraw only on the last expose event
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
case C.FocusIn:
w.w.Event(key.FocusEvent{Focus: true})
w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
case C.FocusOut:
w.w.Event(key.FocusEvent{Focus: false})
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.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
// redraw will be done by a later expose event
case C.SelectionNotify:
@@ -650,7 +685,12 @@ func (h *x11EventHandler) handleEvents() bool {
break
}
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
w.w.Event(clipboard.Event{Text: str})
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 {
@@ -704,7 +744,7 @@ func (h *x11EventHandler) handleEvents() bool {
cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
case C.long(w.atoms.evDelWindow):
w.dead = true
w.shutdown(nil)
return false
}
}
@@ -712,9 +752,7 @@ func (h *x11EventHandler) handleEvents() bool {
return redraw
}
var (
x11Threads sync.Once
)
var x11Threads sync.Once
func init() {
x11Driver = newX11Window
@@ -786,8 +824,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
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()
@@ -823,19 +863,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
// extensions
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
go func() {
w.w.SetDriver(w)
// make the window visible on the screen
C.XMapWindow(dpy, win)
w.Configure(options)
w.w.Event(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
w.setStage(system.StageRunning)
w.loop()
w.w.Event(X11ViewEvent{})
w.w.Event(system.DestroyEvent{Err: nil})
w.destroy()
}()
// 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
}
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Unlicense OR MIT
/*
Package microphone implements permissions to access microphone hardware.
# Android
The following entries will be added to AndroidManifest.xml:
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
RECORD_AUDIO is a "dangerous" permission. See documentation for package
gioui.org/app/permission for more information.
*/
package microphone
+1 -2
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build android || (darwin && ios)
// +build android darwin,ios
package app
@@ -25,6 +24,6 @@ func runMain() {
// Indirect call, since the linker does not know the address of main when
// laying down this package.
fn := mainMain
fn()
go fn()
})
}
+13
View File
@@ -0,0 +1,13 @@
// 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() {}
+1 -1
View File
@@ -175,7 +175,7 @@ func (c *vkContext) refresh(surf vk.Surface, width, height int) error {
if err != nil {
return err
}
minExt, maxExt := caps.MinExtent(), caps.MaxExtent()
minExt, maxExt := vk.SurfaceCapabilitiesMinExtent(caps), vk.SurfaceCapabilitiesMaxExtent(caps)
if width < minExt.X || maxExt.X < width || height < minExt.Y || maxExt.Y < height {
return errOutOfDate
}
-1
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build !novulkan
// +build !novulkan
package app
+422 -599
View File
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -30,6 +30,15 @@ func NewAffine2D(sx, hx, ox, hy, sy, oy float32) Affine2D {
}
}
// 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{
@@ -114,7 +123,7 @@ func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) {
// 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) {
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,
+160 -30
View File
@@ -27,11 +27,11 @@ func TestTransformOffset(t *testing.T) {
p := Point{X: 1, Y: 2}
o := Point{X: 2, Y: -3}
r := Affine2D{}.Offset(o).Transform(p)
r := AffineId().Offset(o).Transform(p)
if !eq(r, Pt(3, -1)) {
t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r)
}
i := Affine2D{}.Offset(o).Invert().Transform(r)
i := AffineId().Offset(o).Invert().Transform(r)
if !eq(i, p) {
t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p)
}
@@ -51,6 +51,9 @@ func TestString(t *testing.T) {
}, {
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 {
@@ -64,11 +67,11 @@ func TestTransformScale(t *testing.T) {
p := Point{X: 1, Y: 2}
s := Point{X: -1, Y: 2}
r := Affine2D{}.Scale(Point{}, s).Transform(p)
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 := Affine2D{}.Scale(Point{}, s).Invert().Transform(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)
}
@@ -78,11 +81,11 @@ func TestTransformRotate(t *testing.T) {
p := Point{X: 1, Y: 0}
a := float32(math.Pi / 2)
r := Affine2D{}.Rotate(Point{}, a).Transform(p)
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 := Affine2D{}.Rotate(Point{}, a).Invert().Transform(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)
}
@@ -91,11 +94,11 @@ func TestTransformRotate(t *testing.T) {
func TestTransformShear(t *testing.T) {
p := Point{X: 1, Y: 1}
r := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Transform(p)
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 := Affine2D{}.Shear(Point{}, math.Pi/4, 0).Invert().Transform(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)
}
@@ -107,11 +110,11 @@ func TestTransformMultiply(t *testing.T) {
s := Point{X: -1, Y: 2}
a := float32(-math.Pi / 2)
r := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Transform(p)
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 := Affine2D{}.Offset(o).Scale(Point{}, s).Rotate(Point{}, a).Shear(Point{}, math.Pi/4, 0).Invert().Transform(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)
}
@@ -163,7 +166,7 @@ func TestPrimes(t *testing.T) {
func TestTransformScaleAround(t *testing.T) {
p := Pt(-1, -1)
target := Pt(-6, -13)
pt := Affine2D{}.Scale(Pt(4, 5), Pt(2, 3)).Transform(p)
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")
@@ -172,7 +175,7 @@ func TestTransformScaleAround(t *testing.T) {
func TestTransformRotateAround(t *testing.T) {
p := Pt(-1, -1)
pt := Affine2D{}.Rotate(Pt(1, 1), -math.Pi/2).Transform(p)
pt := AffineId().Rotate(Pt(1, 1), -math.Pi/2).Transform(p)
target := Pt(-1, 3)
if !eq(pt, target) {
t.Log(pt, "!=", target)
@@ -181,12 +184,12 @@ func TestTransformRotateAround(t *testing.T) {
}
func TestMulOrder(t *testing.T) {
A := Affine2D{}.Offset(Pt(100, 100))
B := Affine2D{}.Scale(Point{}, Pt(2, 2))
A := AffineId().Offset(Pt(100, 100))
B := AffineId().Scale(Point{}, Pt(2, 2))
_ = A
_ = B
T1 := Affine2D{}.Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2))
T1 := AffineId().Offset(Pt(100, 100)).Scale(Point{}, Pt(2, 2))
T2 := B.Mul(A)
if T1 != T2 {
@@ -199,9 +202,9 @@ func TestMulOrder(t *testing.T) {
func BenchmarkTransformOffset(b *testing.B) {
p := Point{X: 1, Y: 2}
o := Point{X: 0.5, Y: 0.5}
aff := Affine2D{}.Offset(o)
aff := AffineId().Offset(o)
for i := 0; i < b.N; i++ {
for b.Loop() {
p = aff.Transform(p)
}
_ = p
@@ -210,8 +213,8 @@ func BenchmarkTransformOffset(b *testing.B) {
func BenchmarkTransformScale(b *testing.B) {
p := Point{X: 1, Y: 2}
s := Point{X: 0.5, Y: 0.5}
aff := Affine2D{}.Scale(Point{}, s)
for i := 0; i < b.N; i++ {
aff := AffineId().Scale(Point{}, s)
for b.Loop() {
p = aff.Transform(p)
}
_ = p
@@ -220,36 +223,163 @@ func BenchmarkTransformScale(b *testing.B) {
func BenchmarkTransformRotate(b *testing.B) {
p := Point{X: 1, Y: 2}
a := float32(math.Pi / 2)
aff := Affine2D{}.Rotate(Point{}, a)
for i := 0; i < b.N; i++ {
aff := AffineId().Rotate(Point{}, a)
for b.Loop() {
p = aff.Transform(p)
}
_ = p
}
func BenchmarkTransformTranslateMultiply(b *testing.B) {
a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5})
a := AffineId().Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
t := AffineId().Offset(Point{X: 0.5, Y: 0.5})
for i := 0; i < b.N; i++ {
for b.Loop() {
a = a.Mul(t)
}
}
func BenchmarkTransformScaleMultiply(b *testing.B) {
a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Scale(Point{}, Point{X: 0.4, Y: -0.5})
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 i := 0; i < b.N; i++ {
for b.Loop() {
a = a.Mul(t)
}
}
func BenchmarkTransformMultiply(b *testing.B) {
a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}).Rotate(Point{}, math.Pi/7)
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 i := 0; i < b.N; i++ {
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)
}
}
Generated
+39 -67
View File
@@ -1,86 +1,58 @@
{
"nodes": {
"android": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1659298920,
"narHash": "sha256-LgRMge8BZUG15EN43iDJOlnEMX1dvRprB7SaoNqgibU=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "d4f20a3cd4ce961bb23b48447457f6810d69ae5e",
"type": "github"
},
"original": {
"owner": "tadfisher",
"repo": "android-nixpkgs",
"type": "github"
}
},
"devshell": {
"inputs": {
"flake-utils": [
"android",
"nixpkgs"
],
"nixpkgs": [
"android",
"nixpkgs"
]
},
"locked": {
"lastModified": 1658746384,
"narHash": "sha256-CCJcoMOcXyZFrV1ag4XMTpAPjLWb4Anbv+ktXFI1ry0=",
"owner": "numtide",
"repo": "devshell",
"rev": "0ffc7937bb5e8141af03d462b468bd071eb18e1b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1659305579,
"narHash": "sha256-SFeQTmh7hc9Y2fSkooHaoS8mDfPa04sfmUCtQ8MA6Pg=",
"lastModified": 1747953325,
"narHash": "sha256-y2ZtlIlNTuVJUZCqzZAhIw5rrKP4DOSklev6c8PyCkQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5857574d45925585baffde730369414319228a84",
"rev": "55d1f923c480dadce40f5231feb472e81b0bab48",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"android": "android",
"nixpkgs": "nixpkgs"
"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"
}
}
},
+38 -47
View File
@@ -3,42 +3,38 @@
description = "Gio build environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
android.url = "github:tadfisher/android-nixpkgs";
android.inputs.nixpkgs.follows = "nixpkgs";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, android }:
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
in
{
devShells = forAllSystems
(system:
let
pkgs = import nixpkgs {
inherit system;
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;
};
android-sdk = android.sdk.${system} (sdkPkgs: with sdkPkgs;
[
build-tools-31-0-0
cmdline-tools-latest
platform-tools
platforms-android-31
ndk-bundle
]);
in
{
default = with pkgs; mkShell
({
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
JAVA_HOME = jdk8.home;
packages = [
android-sdk
jdk8
clang
] ++ (if stdenv.isLinux then [
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
@@ -46,18 +42,13 @@
xorg.libXcursor
xorg.libXfixes
libGL
pkgconfig
] else if stdenv.isDarwin then [
darwin.apple_sdk_11_0.frameworks.Foundation
darwin.apple_sdk_11_0.frameworks.Metal
darwin.apple_sdk_11_0.frameworks.QuartzCore
darwin.apple_sdk_11_0.frameworks.AppKit
darwin.apple_sdk_11_0.MacOSX-SDK
] else [ ]);
} // (if stdenv.isLinux then {
LD_LIBRARY_PATH = "${vulkan-loader}/lib";
} else { }));
}
);
};
pkg-config
] else
[ ]);
} // (if stdenv.isLinux then {
LD_LIBRARY_PATH = "${vulkan-loader}/lib";
} else
{ }));
};
});
}
+1 -1
View File
@@ -34,7 +34,7 @@ type Font struct {
// 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
Face() *font.Face
}
// Typeface identifies a list of font families to attempt to use for displaying
+42 -44
View File
@@ -16,23 +16,21 @@ import (
_ "image/png"
giofont "gioui.org/font"
"github.com/go-text/typesetting/font"
fontapi "github.com/go-text/typesetting/opentype/api/font"
"github.com/go-text/typesetting/opentype/api/metadata"
"github.com/go-text/typesetting/opentype/loader"
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 font.Font
face *fontapi.Font
font giofont.Font
}
// Parse constructs a Face from source bytes.
func Parse(src []byte) (Face, error) {
ld, err := loader.NewLoader(bytes.NewReader(src))
ld, err := opentype.NewLoader(bytes.NewReader(src))
if err != nil {
return Face{}, err
}
@@ -49,11 +47,11 @@ func Parse(src []byte) (Face, error) {
// 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 metadata.
// inferred font font.
// BUG(whereswaldon): the only Variant that can be detected automatically is
// "Mono".
func ParseCollection(src []byte) ([]giofont.FontFace, error) {
lds, err := loader.NewLoaders(bytes.NewReader(src))
lds, err := opentype.NewLoaders(bytes.NewReader(src))
if err != nil {
return nil, err
}
@@ -76,7 +74,7 @@ func ParseCollection(src []byte) ([]giofont.FontFace, error) {
return out, nil
}
func DescriptionToFont(md metadata.Description) giofont.Font {
func DescriptionToFont(md fontapi.Description) giofont.Font {
return giofont.Font{
Typeface: giofont.Typeface(md.Family),
Style: gioStyle(md.Aspect.Style),
@@ -84,31 +82,31 @@ func DescriptionToFont(md metadata.Description) giofont.Font {
}
}
func FontToDescription(font giofont.Font) metadata.Description {
return metadata.Description{
func FontToDescription(font giofont.Font) fontapi.Description {
return fontapi.Description{
Family: string(font.Typeface),
Aspect: metadata.Aspect{
Aspect: fontapi.Aspect{
Style: mdStyle(font.Style),
Weight: mdWeight(font.Weight),
},
}
}
// parseLoader parses the contents of the loader into a face and its metadata.
func parseLoader(ld *loader.Loader) (font.Font, giofont.Font, error) {
// 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(metadata.Metadata(ld))
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() font.Face {
return &fontapi.Face{Font: f.face}
func (f Face) Face() *fontapi.Face {
return fontapi.NewFace(f.face)
}
// FontFace returns a text.Font with populated font metadata for the
@@ -119,74 +117,74 @@ func (f Face) Font() giofont.Font {
return f.font
}
func gioStyle(s metadata.Style) giofont.Style {
func gioStyle(s fontapi.Style) giofont.Style {
switch s {
case metadata.StyleItalic:
case fontapi.StyleItalic:
return giofont.Italic
case metadata.StyleNormal:
case fontapi.StyleNormal:
fallthrough
default:
return giofont.Regular
}
}
func mdStyle(g giofont.Style) metadata.Style {
func mdStyle(g giofont.Style) fontapi.Style {
switch g {
case giofont.Italic:
return metadata.StyleItalic
return fontapi.StyleItalic
case giofont.Regular:
fallthrough
default:
return metadata.StyleNormal
return fontapi.StyleNormal
}
}
func gioWeight(w metadata.Weight) giofont.Weight {
func gioWeight(w fontapi.Weight) giofont.Weight {
switch w {
case metadata.WeightThin:
case fontapi.WeightThin:
return giofont.Thin
case metadata.WeightExtraLight:
case fontapi.WeightExtraLight:
return giofont.ExtraLight
case metadata.WeightLight:
case fontapi.WeightLight:
return giofont.Light
case metadata.WeightNormal:
case fontapi.WeightNormal:
return giofont.Normal
case metadata.WeightMedium:
case fontapi.WeightMedium:
return giofont.Medium
case metadata.WeightSemibold:
case fontapi.WeightSemibold:
return giofont.SemiBold
case metadata.WeightBold:
case fontapi.WeightBold:
return giofont.Bold
case metadata.WeightExtraBold:
case fontapi.WeightExtraBold:
return giofont.ExtraBold
case metadata.WeightBlack:
case fontapi.WeightBlack:
return giofont.Black
default:
return giofont.Normal
}
}
func mdWeight(g giofont.Weight) metadata.Weight {
func mdWeight(g giofont.Weight) fontapi.Weight {
switch g {
case giofont.Thin:
return metadata.WeightThin
return fontapi.WeightThin
case giofont.ExtraLight:
return metadata.WeightExtraLight
return fontapi.WeightExtraLight
case giofont.Light:
return metadata.WeightLight
return fontapi.WeightLight
case giofont.Normal:
return metadata.WeightNormal
return fontapi.WeightNormal
case giofont.Medium:
return metadata.WeightMedium
return fontapi.WeightMedium
case giofont.SemiBold:
return metadata.WeightSemibold
return fontapi.WeightSemibold
case giofont.Bold:
return metadata.WeightBold
return fontapi.WeightBold
case giofont.ExtraBold:
return metadata.WeightExtraBold
return fontapi.WeightExtraBold
case giofont.Black:
return metadata.WeightBlack
return fontapi.WeightBlack
default:
return metadata.WeightNormal
return fontapi.WeightNormal
}
}
+107 -99
View File
@@ -18,6 +18,7 @@ import (
"gioui.org/f32"
"gioui.org/internal/fling"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/op"
@@ -37,31 +38,31 @@ type Hover struct {
// Add the gesture to detect hovering over the current pointer area.
func (h *Hover) Add(ops *op.Ops) {
pointer.InputOp{
Tag: h,
Types: pointer.Enter | pointer.Leave,
}.Add(ops)
event.Op(ops, h)
}
// Hovered returns whether a pointer is inside the area.
func (h *Hover) Hovered(q event.Queue) bool {
for _, ev := range q.Events(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.Type {
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
}
h.pid = e.PointerID
h.entered = true
}
}
return h.entered
@@ -87,10 +88,10 @@ type Click struct {
}
// ClickEvent represent a click action, either a
// TypePress for the beginning of a click or a
// TypeClick for a completed click.
// KindPress for the beginning of a click or a
// KindClick for a completed click.
type ClickEvent struct {
Type ClickType
Kind ClickKind
Position image.Point
Source pointer.Source
Modifiers key.Modifiers
@@ -99,7 +100,7 @@ type ClickEvent struct {
NumClicks int
}
type ClickType uint8
type ClickKind uint8
// Drag detects drag gestures in the form of pointer.Drag events.
type Drag struct {
@@ -107,7 +108,6 @@ type Drag struct {
pressed bool
pid pointer.ID
start f32.Point
grab bool
}
// Scroll detects scroll gestures and reduces them to
@@ -115,11 +115,9 @@ type Drag struct {
// movements as well as drag and fling touch gestures.
type Scroll struct {
dragging bool
axis Axis
estimator fling.Extrapolation
flinger fling.Animation
pid pointer.ID
grab bool
last int
// Leftover scroll.
scroll float32
@@ -136,15 +134,15 @@ const (
)
const (
// TypePress is reported for the first pointer
// KindPress is reported for the first pointer
// press.
TypePress ClickType = iota
// TypeClick is reported when a click action
KindPress ClickKind = iota
// KindClick is reported when a click action
// is complete.
TypeClick
// TypeCancel is reported when the gesture is
KindClick
// KindCancel is reported when the gesture is
// cancelled.
TypeCancel
KindCancel
)
const (
@@ -161,10 +159,7 @@ const touchSlop = unit.Dp(3)
// Add the handler to the operation list to receive click events.
func (c *Click) Add(ops *op.Ops) {
pointer.InputOp{
Tag: c,
Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
}.Add(ops)
event.Op(ops, c)
}
// Hovered returns whether a pointer is inside the area.
@@ -177,24 +172,36 @@ func (c *Click) Pressed() bool {
return c.pressed
}
// Events returns the next click events, if any.
func (c *Click) Events(q event.Queue) []ClickEvent {
var events []ClickEvent
for _, evt := range q.Events(c) {
// 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.Type {
switch e.Kind {
case pointer.Release:
if !c.pressed || c.pid != e.PointerID {
break
}
c.pressed = false
if !c.entered || c.hovered {
events = append(events, ClickEvent{Type: TypeClick, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
return ClickEvent{
Kind: KindClick,
Position: e.Position.Round(),
Source: e.Source,
Modifiers: e.Modifiers,
NumClicks: c.clicks,
}, true
} else {
events = append(events, ClickEvent{Type: TypeCancel})
return ClickEvent{Kind: KindCancel}, true
}
case pointer.Cancel:
wasPressed := c.pressed
@@ -202,7 +209,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
c.hovered = false
c.entered = false
if wasPressed {
events = append(events, ClickEvent{Type: TypeCancel})
return ClickEvent{Kind: KindCancel}, true
}
case pointer.Press:
if c.pressed {
@@ -211,12 +218,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary {
break
}
if !c.hovered {
c.pid = e.PointerID
}
if c.pid != e.PointerID {
break
}
c.pid = e.PointerID
c.pressed = true
if e.Time-c.clickedAt < doubleClickDuration {
c.clicks++
@@ -224,7 +226,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
c.clicks = 1
}
c.clickedAt = e.Time
events = append(events, ClickEvent{Type: TypePress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
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
@@ -242,25 +244,16 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
}
}
}
return events
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 io/pointer.InputOp.
func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
oph := pointer.InputOp{
Tag: s,
Grab: s.grab,
Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
ScrollBounds: bounds,
}
oph.Add(ops)
if s.flinger.Active() {
op.InvalidateOp{}.Add(ops)
}
// as defined in [pointer.Filter].
func (s *Scroll) Add(ops *op.Ops) {
event.Op(ops, s)
}
// Stop any remaining fling movement.
@@ -268,20 +261,25 @@ func (s *Scroll) Stop() {
s.flinger = fling.Animation{}
}
// Scroll detects the scrolling distance from the available events and
// ongoing fling gestures.
func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
if s.axis != axis {
s.axis = axis
return 0
}
// 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
for _, evt := range q.Events(s) {
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.Type {
switch e.Kind {
case pointer.Press:
if s.dragging {
break
@@ -293,7 +291,7 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
}
s.Stop()
s.estimator = fling.Extrapolation{}
v := s.val(e.Position)
v := s.val(axis, e.Position)
s.last = int(math.Round(float64(v)))
s.estimator.Sample(e.Time, v)
s.dragging = true
@@ -309,13 +307,14 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
fallthrough
case pointer.Cancel:
s.dragging = false
s.grab = false
case pointer.Scroll:
switch s.axis {
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)
@@ -324,14 +323,14 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
if !s.dragging || s.pid != e.PointerID {
continue
}
val := s.val(e.Position)
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 {
s.grab = true
q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID})
}
} else {
s.last = v
@@ -340,14 +339,22 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
}
}
total += s.flinger.Tick(t)
if s.flinger.Active() {
q.Execute(op.InvalidateCmd{})
}
return total
}
func (s *Scroll) val(p f32.Point) float32 {
if s.axis == Horizontal {
func (s *Scroll) val(axis Axis, p f32.Point) float32 {
switch axis {
case Horizontal:
return p.X
} else {
case Vertical:
return p.Y
case Both:
return p.X + p.Y
default:
return 0
}
}
@@ -365,23 +372,25 @@ func (s *Scroll) State() ScrollState {
// Add the handler to the operation list to receive drag events.
func (d *Drag) Add(ops *op.Ops) {
pointer.InputOp{
Tag: d,
Grab: d.grab,
Types: pointer.Press | pointer.Drag | pointer.Release,
}.Add(ops)
event.Op(ops, d)
}
// Events returns the next drag events, if any.
func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
var events []pointer.Event
for _, e := range q.Events(d) {
e, ok := e.(pointer.Event)
// 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.Type {
switch e.Kind {
case pointer.Press:
if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
continue
@@ -409,7 +418,7 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
diff := e.Position.Sub(d.start)
slop := cfg.Dp(touchSlop)
if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
d.grab = true
q.Execute(pointer.GrabCmd{Tag: d, ID: e.PointerID})
}
}
case pointer.Release, pointer.Cancel:
@@ -418,13 +427,12 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
continue
}
d.dragging = false
d.grab = false
}
events = append(events, e)
return e, true
}
return events
return pointer.Event{}, false
}
// Dragging reports whether it is currently in use.
@@ -444,16 +452,16 @@ func (a Axis) String() string {
}
}
func (ct ClickType) String() string {
func (ct ClickKind) String() string {
switch ct {
case TypePress:
return "TypePress"
case TypeClick:
return "TypeClick"
case TypeCancel:
return "TypeCancel"
case KindPress:
return "KindPress"
case KindClick:
return "KindClick"
case KindCancel:
return "KindCancel"
default:
panic("invalid ClickType")
panic("invalid ClickKind")
}
}
+93 -21
View File
@@ -9,8 +9,8 @@ import (
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/op"
"gioui.org/op/clip"
)
@@ -22,20 +22,21 @@ func TestHover(t *testing.T) {
stack := clip.Rect(rect).Push(ops)
h.Add(ops)
stack.Pop()
r := new(router.Router)
r := new(input.Router)
h.Update(r.Source())
r.Frame(ops)
r.Queue(
pointer.Event{Type: pointer.Move, Position: f32.Pt(30, 30)},
pointer.Event{Kind: pointer.Move, Position: f32.Pt(30, 30)},
)
if !h.Hovered(r) {
if !h.Update(r.Source()) {
t.Fatal("expected hovered")
}
r.Queue(
pointer.Event{Type: pointer.Move, Position: f32.Pt(50, 50)},
pointer.Event{Kind: pointer.Move, Position: f32.Pt(50, 50)},
)
if h.Hovered(r) {
if h.Update(r.Source()) {
t.Fatal("expected not hovered")
}
}
@@ -71,12 +72,21 @@ func TestMouseClicks(t *testing.T) {
var ops op.Ops
click.Add(&ops)
var r router.Router
var r input.Router
click.Update(r.Source())
r.Frame(&ops)
r.Queue(tc.events...)
events := click.Events(&r)
clicks := filterMouseClicks(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)
}
@@ -90,9 +100,81 @@ func TestMouseClicks(t *testing.T) {
}
}
func TestClickPointerIDReassignment(t *testing.T) {
// A Click must accept a Press from a PointerID that differs from the
// one its hovered state was previously associated with. Some backends
// reassign a single physical pointer's ID over its lifetime — e.g. the
// Windows pointer API across focus changes — and locking the gesture
// to the first observed ID would silently drop every subsequent press.
//
// The sequence below puts the gesture into the buggy state through
// public events alone: a press under PointerID 1 starts an active
// press cycle, a Move under PointerID 2 arrives mid-press (which the
// router routes as an Enter for PID 2 but the gesture's Enter handler
// is a no-op for pid while pressed), then PID 1 releases. After this,
// the router has the gesture entered for PID 2 (so the next event
// under PID 2 won't trigger another Enter) but the gesture itself
// still has pid=1.
var click Click
var ops op.Ops
rect := image.Rect(0, 0, 100, 100)
stack := clip.Rect(rect).Push(&ops)
click.Add(&ops)
stack.Pop()
var r input.Router
click.Update(r.Source())
r.Frame(&ops)
drain := func() {
for {
if _, ok := click.Update(r.Source()); !ok {
return
}
}
}
// Press under PointerID 1.
r.Queue(
pointer.Event{Kind: pointer.Move, Source: pointer.Mouse, Position: f32.Pt(50, 50), PointerID: 1},
pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Buttons: pointer.ButtonPrimary, Position: f32.Pt(50, 50), PointerID: 1},
)
drain()
// Move under PointerID 2 while PointerID 1 is still pressed. The
// router records the gesture as entered for PointerID 2 but the
// gesture's Enter handler is a no-op for pid because c.pressed.
r.Queue(pointer.Event{Kind: pointer.Move, Source: pointer.Mouse, Position: f32.Pt(50, 50), PointerID: 2})
drain()
// Release PointerID 1. PointerID 1's press tracking ends; the
// gesture's recorded pid stays at 1.
r.Queue(pointer.Event{Kind: pointer.Release, Source: pointer.Mouse, Position: f32.Pt(50, 50), PointerID: 1})
drain()
// Press under PointerID 2. The router won't refire Enter for PID 2
// (the gesture is already in PID 2's entered set), so the gesture's
// only chance to refresh its pid is the Press handler itself.
r.Queue(pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Buttons: pointer.ButtonPrimary, Position: f32.Pt(50, 50), PointerID: 2})
var sawPress bool
for {
ev, ok := click.Update(r.Source())
if !ok {
break
}
if ev.Kind == KindPress {
sawPress = true
}
}
if !sawPress {
t.Fatal("expected KindPress for press under reassigned PointerID; gesture dropped the press because of stale recorded pid")
}
}
func mouseClickEvents(times ...time.Duration) []event.Event {
press := pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
}
@@ -101,18 +183,8 @@ func mouseClickEvents(times ...time.Duration) []event.Event {
press := press
press.Time = t
release := press
release.Type = pointer.Release
release.Kind = pointer.Release
events = append(events, press, release)
}
return events
}
func filterMouseClicks(events []ClickEvent) []ClickEvent {
var clicks []ClickEvent
for _, ev := range events {
if ev.Type == TypeClick {
clicks = append(clicks, ev)
}
}
return clicks
}
+8 -8
View File
@@ -1,16 +1,16 @@
module gioui.org
go 1.19
go 1.24.0
require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.8
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
golang.org/x/image v0.5.0
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64
github.com/go-text/typesetting v0.3.4
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/image v0.26.0
golang.org/x/sys v0.39.0
golang.org/x/text v0.32.0
)
require golang.org/x/text v0.7.0
require golang.org/x/net v0.48.0
+16 -38
View File
@@ -1,43 +1,21 @@
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80=
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+15 -10
View File
@@ -8,8 +8,13 @@ import (
"gioui.org/internal/f32"
)
type resourceCache struct {
res map[interface{}]resourceCacheValue
type textureCacheKey struct {
filter byte
handle any
}
type textureCache struct {
res map[textureCacheKey]resourceCacheValue
}
type resourceCacheValue struct {
@@ -37,13 +42,13 @@ type opCacheValue struct {
keep bool
}
func newResourceCache() *resourceCache {
return &resourceCache{
res: make(map[interface{}]resourceCacheValue),
func newTextureCache() *textureCache {
return &textureCache{
res: make(map[textureCacheKey]resourceCacheValue),
}
}
func (r *resourceCache) get(key interface{}) (resource, bool) {
func (r *textureCache) get(key textureCacheKey) (resource, bool) {
v, exists := r.res[key]
if !exists {
return nil, false
@@ -55,17 +60,17 @@ func (r *resourceCache) get(key interface{}) (resource, bool) {
return v.resource, exists
}
func (r *resourceCache) put(key interface{}, val resource) {
func (r *textureCache) put(key textureCacheKey, val resource) {
v, exists := r.res[key]
if exists && v.used {
panic(fmt.Errorf("key exists, %p", key))
panic(fmt.Errorf("key exists, %v", key))
}
v.used = true
v.resource = val
r.res[key] = v
}
func (r *resourceCache) frame() {
func (r *textureCache) frame() {
for k, v := range r.res {
if v.used {
v.used = false
@@ -77,7 +82,7 @@ func (r *resourceCache) frame() {
}
}
func (r *resourceCache) release() {
func (r *textureCache) release() {
for _, v := range r.res {
v.resource.release()
}
-24
View File
@@ -1,24 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package gpu
import "testing"
func BenchmarkResourceCache(b *testing.B) {
offset := 0
const N = 100
cache := newResourceCache()
for i := 0; i < b.N; i++ {
// half are the same and half updated
for k := 0; k < N; k++ {
cache.put(offset+k, nullResource{})
}
cache.frame()
offset += N / 2
}
}
type nullResource struct{}
func (nullResource) release() {}
+6 -6
View File
@@ -19,10 +19,10 @@ type quadSplitter struct {
func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) {
// inlined code:
// encodeVertex(data, meta, -1, 1, from, ctrl, to)
// 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)
// encodeVertex(data[vertStride*3:], meta, -1, 1, from, ctrl, to)
// this code needs to stay in sync with `vertex.encode`.
bo := binary.LittleEndian
@@ -48,10 +48,10 @@ func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) {
}
const (
nwCorner = 1*0.25 + 0*0.5
neCorner = 1*0.25 + 1*0.5
swCorner = 0*0.25 + 0*0.5
seCorner = 0*0.25 + 1*0.5
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) {
+1 -1
View File
@@ -10,7 +10,7 @@ import (
func BenchmarkEncodeQuadTo(b *testing.B) {
var data [vertStride * 4]byte
for i := 0; i < b.N; i++ {
for i := 0; b.Loop(); i++ {
v := float32(i)
encodeQuadTo(data[:], 123,
f32.Point{X: v, Y: v},
-2202
View File
File diff suppressed because it is too large Load Diff
-129
View File
@@ -1,129 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package gpu
import (
"unsafe"
"gioui.org/cpu"
)
// This file contains code specific to running compute shaders on the CPU.
// dispatcher dispatches CPU compute programs across multiple goroutines.
type dispatcher struct {
// done is notified when a worker completes its work slice.
done chan struct{}
// work receives work slice indices. It is closed when the dispatcher is released.
work chan work
// dispatch receives compute jobs, which is then split among workers.
dispatch chan dispatch
// sync receives notification when a Sync completes.
sync chan struct{}
}
type work struct {
ctx *cpu.DispatchContext
index int
}
type dispatch struct {
_type jobType
program *cpu.ProgramInfo
descSet unsafe.Pointer
x, y, z int
}
type jobType uint8
const (
jobDispatch jobType = iota
jobBarrier
jobSync
)
func newDispatcher(workers int) *dispatcher {
d := &dispatcher{
work: make(chan work, workers),
done: make(chan struct{}, workers),
// Leave some room to avoid blocking calls to Dispatch.
dispatch: make(chan dispatch, 20),
sync: make(chan struct{}),
}
for i := 0; i < workers; i++ {
go d.worker()
}
go d.dispatcher()
return d
}
func (d *dispatcher) dispatcher() {
defer close(d.work)
var free []*cpu.DispatchContext
defer func() {
for _, ctx := range free {
ctx.Free()
}
}()
var used []*cpu.DispatchContext
for job := range d.dispatch {
switch job._type {
case jobDispatch:
if len(free) == 0 {
free = append(free, cpu.NewDispatchContext())
}
ctx := free[len(free)-1]
free = free[:len(free)-1]
used = append(used, ctx)
ctx.Prepare(cap(d.work), job.program, job.descSet, job.x, job.y, job.z)
for i := 0; i < cap(d.work); i++ {
d.work <- work{
ctx: ctx,
index: i,
}
}
case jobBarrier:
// Wait for all outstanding dispatches to complete.
for i := 0; i < len(used)*cap(d.work); i++ {
<-d.done
}
free = append(free, used...)
used = used[:0]
case jobSync:
d.sync <- struct{}{}
}
}
}
func (d *dispatcher) worker() {
thread := cpu.NewThreadContext()
defer thread.Free()
for w := range d.work {
w.ctx.Dispatch(w.index, thread)
d.done <- struct{}{}
}
}
func (d *dispatcher) Barrier() {
d.dispatch <- dispatch{_type: jobBarrier}
}
func (d *dispatcher) Sync() {
d.dispatch <- dispatch{_type: jobSync}
<-d.sync
}
func (d *dispatcher) Dispatch(program *cpu.ProgramInfo, descSet unsafe.Pointer, x, y, z int) {
d.dispatch <- dispatch{
_type: jobDispatch,
program: program,
descSet: descSet,
x: x,
y: y,
z: z,
}
}
func (d *dispatcher) Stop() {
close(d.dispatch)
}
+192 -143
View File
@@ -9,12 +9,13 @@ package gpu
import (
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"math"
"os"
"reflect"
"slices"
"time"
"unsafe"
@@ -44,14 +45,10 @@ type GPU interface {
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
// Profile returns the last available profiling information. Profiling
// information is requested when Frame sees an io/profile.Op, and the result
// is available through Profile at some later time.
Profile() string
}
type gpu struct {
cache *resourceCache
cache *textureCache
profile string
timers *timers
@@ -73,7 +70,6 @@ type renderer struct {
}
type drawOps struct {
profile bool
reader ops.Reader
states []f32.Affine2D
transStack []f32.Affine2D
@@ -186,10 +182,16 @@ type material struct {
uvTrans f32.Affine2D
}
const (
filterLinear = 0
filterNearest = 1
)
// imageOpData is the shadow of paint.ImageOp.
type imageOpData struct {
src *image.RGBA
handle interface{}
handle any
filter byte
}
type linearGradientOpData struct {
@@ -199,7 +201,7 @@ type linearGradientOpData struct {
color2 color.NRGBA
}
func decodeImageOp(data []byte, refs []interface{}) imageOpData {
func decodeImageOp(data []byte, refs []any) imageOpData {
handle := refs[1]
if handle == nil {
return imageOpData{}
@@ -207,6 +209,7 @@ func decodeImageOp(data []byte, refs []interface{}) imageOpData {
return imageOpData{
src: refs[0].(*image.RGBA),
handle: handle,
filter: data[1],
}
}
@@ -259,7 +262,7 @@ type texture struct {
type blitter struct {
ctx driver.Device
viewport image.Point
pipelines [3]*pipeline
pipelines [2][3]*pipeline
colUniforms *blitColUniforms
texUniforms *blitTexUniforms
linearGradientUniforms *blitLinearGradientUniforms
@@ -341,18 +344,17 @@ func New(api API) (GPU, error) {
func NewWithDevice(d driver.Device) (GPU, error) {
d.BeginFrame(nil, false, image.Point{})
defer d.EndFrame()
forceCompute := os.Getenv("GIORENDERER") == "forcecompute"
feats := d.Caps().Features
switch {
case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB):
case feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB):
return newGPU(d)
}
return newCompute(d)
return nil, errors.New("no available GPU driver")
}
func newGPU(ctx driver.Device) (*gpu, error) {
g := &gpu{
cache: newResourceCache(),
cache: newTextureCache(),
}
g.drawOps.pathCache = newOpCache()
if err := g.init(ctx); err != nil {
@@ -392,7 +394,7 @@ func (g *gpu) collect(viewport image.Point, frameOps *op.Ops) {
g.renderer.pather.viewport = viewport
g.drawOps.reset(viewport)
g.drawOps.collect(frameOps, viewport)
if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
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()
@@ -418,9 +420,9 @@ func (g *gpu) frame(target RenderTarget) error {
g.stencilTimer.end()
g.coverTimer.begin()
g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
g.renderer.prepareDrawOps(g.drawOps.imageOps)
g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
g.renderer.drawLayers(g.cache, g.drawOps.layers, g.drawOps.imageOps)
g.renderer.drawLayers(g.drawOps.layers, g.drawOps.imageOps)
d := driver.LoadDesc{
ClearColor: g.drawOps.clearColor,
}
@@ -430,14 +432,14 @@ func (g *gpu) frame(target RenderTarget) error {
}
g.ctx.BeginRenderPass(defFBO, d)
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
g.renderer.drawOps(g.cache, false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
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 g.drawOps.profile && g.timers.ready() {
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
@@ -453,20 +455,38 @@ func (g *gpu) Profile() string {
return g.profile
}
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
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(data.handle)
t, exists := cache.get(key)
if !exists {
t = &texture{
src: data.src,
}
cache.put(data.handle, t)
cache.put(key, t)
}
tex = t.(*texture)
if tex.tex != nil {
return tex.tex
}
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA, data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinearMipmapLinear, driver.FilterLinear, driver.BufferBindingTexture)
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)
}
@@ -528,7 +548,7 @@ func newBlitter(ctx driver.Device) *blitter {
b.texUniforms = new(blitTexUniforms)
b.linearGradientUniforms = new(blitLinearGradientUniforms)
pipelines, err := createColorPrograms(ctx, gio.Shader_blit_vert, gio.Shader_blit_frag,
[3]interface{}{b.colUniforms, b.linearGradientUniforms, b.texUniforms},
[3]any{b.colUniforms, b.linearGradientUniforms, b.texUniforms},
)
if err != nil {
panic(err)
@@ -540,12 +560,24 @@ func newBlitter(ctx driver.Device) *blitter {
func (b *blitter) release() {
b.quadVerts.Release()
for _, p := range b.pipelines {
p.Release()
for _, p := range p {
p.Release()
}
}
}
func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) ([3]*pipeline, error) {
var pipelines [3]*pipeline
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,
@@ -563,86 +595,76 @@ func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.
return pipelines, err
}
defer vsh.Release()
{
fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
if err != nil {
return pipelines, err
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}
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: driver.TextureFormatOutput,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
return pipelines, err
{
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
if u := uniforms[materialTexture]; u != nil {
vertBuffer = newUniformBuffer(b, u)
{
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}
}
pipelines[materialTexture] = &pipeline{pipe, vertBuffer}
}
{
var vertBuffer *uniformBuffer
fsh, err := b.NewFragmentShader(fsSrc[materialColor])
if err != nil {
pipelines[materialTexture].Release()
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: driver.TextureFormatOutput,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
pipelines[materialTexture].Release()
return pipelines, err
}
if u := uniforms[materialColor]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[materialColor] = &pipeline{pipe, vertBuffer}
}
{
var vertBuffer *uniformBuffer
fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient])
if err != nil {
pipelines[materialTexture].Release()
pipelines[materialColor].Release()
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: driver.TextureFormatOutput,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
pipelines[materialTexture].Release()
pipelines[materialColor].Release()
return pipelines, err
}
if u := uniforms[materialLinearGradient]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[materialLinearGradient] = &pipeline{pipe, vertBuffer}
}
if err != nil {
for _, p := range pipelines {
p.Release()
}
return pipelines, err
}
return pipelines, nil
}
@@ -801,7 +823,7 @@ func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
layers[l.parent].clip = b.Union(l.clip)
}
if l.clip.Empty() {
layers = append(layers[:i], layers[i+1:]...)
layers = slices.Delete(layers, i, i+1)
}
}
// Pack layers.
@@ -824,7 +846,7 @@ func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
return layers
}
func (r *renderer) drawLayers(cache *resourceCache, layers []opacityLayer, ops []imageOp) {
func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
if len(r.layers.sizes) == 0 {
return
}
@@ -845,12 +867,12 @@ func (r *renderer) drawLayers(cache *resourceCache, layers []opacityLayer, ops [
Min: l.place.Pos,
Max: l.place.Pos.Add(l.clip.Size()),
}
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y)
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy())
f := r.layerFBOs.fbos[fbo]
r.drawOps(cache, true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
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.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset)
uvTrans := f32.AffineId().Scale(f32.Point{}, uvScale).Offset(uvOffset)
// Replace layer ops with one textured op.
ops[l.opStart] = imageOp{
clip: l.clip,
@@ -870,7 +892,6 @@ func (r *renderer) drawLayers(cache *resourceCache, layers []opacityLayer, ops [
}
func (d *drawOps) reset(viewport image.Point) {
d.profile = false
d.viewport = viewport
d.imageOps = d.imageOps[:0]
d.pathOps = d.pathOps[:0]
@@ -911,7 +932,7 @@ func (d *drawOps) newPathOp() *pathOp {
return &d.pathOpCache[len(d.pathOpCache)-1]
}
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point, push bool) {
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point) {
npath := d.newPathOp()
*npath = pathOp{
parent: state.cpath,
@@ -936,7 +957,9 @@ func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds
func (d *drawOps) save(id int, state f32.Affine2D) {
if extra := id - len(d.states) + 1; extra > 0 {
d.states = append(d.states, make([]f32.Affine2D, extra)...)
for range extra {
d.states = append(d.states, f32.AffineId())
}
}
d.states[id] = state
}
@@ -951,12 +974,13 @@ func (k opKey) SetTransform(t f32.Affine2D) opKey {
}
func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
var (
quads quadsOp
state drawState
)
var quads quadsOp
state := drawState{
t: f32.AffineId(),
}
reset := func() {
state = drawState{
t: f32.AffineId(),
color: color.NRGBA{A: 0xff},
}
}
@@ -964,8 +988,6 @@ func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
loop:
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeProfile:
d.profile = true
case ops.TypeTransform:
dop, push := ops.DecodeTransform(encOp.Data)
if push {
@@ -1014,7 +1036,7 @@ loop:
op.Decode(encOp.Data)
quads.key.outline = op.Outline
bounds := f32.FRect(op.Bounds)
trans, off := state.t.Split()
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
@@ -1037,8 +1059,9 @@ loop:
} 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, true)
d.addClipPath(&state, quads.aux, quads.key, bounds, off)
quads = quadsOp{}
case ops.TypePopClip:
state.cpath = state.cpath.parent
@@ -1060,7 +1083,7 @@ loop:
// 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 := state.t.Split()
t, off := transformOffset(state.t)
// Fill the clip area, unless the material is a (bounded) image.
// TODO: Find a tighter bound.
inf := float32(1e6)
@@ -1082,8 +1105,8 @@ loop:
// The paint operation is sheared or rotated, add a clip path representing
// this transformed rectangle.
k := opKey{Key: encOp.Key}
k.SetTransform(t) // TODO: This call has no effect.
d.addClipPath(&state, clipData, k, bnd, off, false)
k = k.SetTransform(t)
d.addClipPath(&state, clipData, k, bnd, off)
}
bounds := cl.Round()
@@ -1143,6 +1166,7 @@ func expandPathOp(p *pathOp, clip image.Rectangle) {
func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle) material {
m := material{
opacity: 1.,
uvTrans: f32.AffineId(),
}
switch d.matType {
case materialColor:
@@ -1176,13 +1200,13 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
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.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset))
m.uvTrans = partTrans.Mul(f32.AffineId().Scale(f32.Point{}, uvScale).Offset(uvOffset))
m.data = d.image
}
return m
}
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) {
for i := range ops {
img := &ops[i]
m := img.material
@@ -1192,7 +1216,7 @@ func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
}
}
func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
func (r *renderer) prepareDrawOps(ops []imageOp) {
for _, img := range ops {
m := img.material
switch m.material {
@@ -1213,7 +1237,7 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
}
}
func (r *renderer) drawOps(cache *resourceCache, isFBO bool, opOff image.Point, viewport image.Point, ops []imageOp) {
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]
@@ -1227,9 +1251,13 @@ func (r *renderer) drawOps(cache *resourceCache, isFBO bool, opOff image.Point,
scale, off := clipSpaceTransform(drc, viewport)
var fbo FBO
fboIdx := 0
if isFBO {
fboIdx = 1
}
switch img.clipType {
case clipTypeNone:
p := r.blitter.pipelines[m.material]
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)
@@ -1248,7 +1276,7 @@ func (r *renderer) drawOps(cache *resourceCache, isFBO bool, opOff image.Point,
Max: img.place.Pos.Add(drc.Size()),
}
coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size)
p := r.pather.coverer.pipelines[m.material]
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)
@@ -1256,7 +1284,11 @@ func (r *renderer) drawOps(cache *resourceCache, isFBO bool, opOff image.Point,
}
func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) {
p := b.pipelines[mat]
fboIdx := 0
if fbo {
fboIdx = 1
}
p := b.pipelines[fboIdx][mat]
b.ctx.BindPipeline(p.pipeline)
var uniforms *blitUniforms
switch mat {
@@ -1289,7 +1321,7 @@ func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2
// newUniformBuffer creates a new GPU uniform buffer backed by the
// structure uniformBlock points to.
func newUniformBuffer(b driver.Device, uniformBlock interface{}) *uniformBuffer {
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()
@@ -1343,7 +1375,7 @@ func gradientSpaceTransform(clip image.Rectangle, off f32.Point, stop1, stop2 f3
// TODO: optimize
zp := f32.Point{}
return f32.Affine2D{}.
return f32.AffineId().
Scale(zp, layout.FPt(clip.Size())). // scale to pixel space
Offset(zp.Sub(off).Add(layout.FPt(clip.Min))). // offset to clip space
Offset(zp.Sub(stop1)). // offset to first stop point
@@ -1457,7 +1489,7 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str
// 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 = bo.Uint32(pathData)
qs.contour = binary.LittleEndian.Uint32(pathData)
cmd := ops.DecodeCommand(pathData[4:])
switch cmd.Op() {
case scene.OpLine:
@@ -1493,12 +1525,10 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
// create GPU vertices for transformed r, find the bounds and establish texture transform.
func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) {
if isPureOffset(tr) {
// fast-path to allow blitting of pure rectangles
_, _, ox, _, _, oy := tr.Elems()
off := f32.Pt(ox, oy)
bnd.Min = r.Min.Add(off)
bnd.Max = r.Max.Add(off)
ptr = f32.AffineId()
if tr == f32.AffineId() {
// fast-path to allow blitting of pure rectangles.
bnd = r
return
}
@@ -1545,10 +1575,29 @@ func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, tr f32.Affine2D) (au
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
return aux, bnd, ptr
}
func isPureOffset(t f32.Affine2D) bool {
a, b, _, d, e, _ := t.Elems()
return a == 1 && b == 0 && d == 0 && e == 1
// 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, f32.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 := f32.Pt(float32(iox), float32(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
}
+5 -3
View File
@@ -21,8 +21,10 @@ import (
var dumpImages = flag.Bool("saveimages", false, "save test images")
var clearCol = color.NRGBA{A: 0xff, R: 0xde, G: 0xad, B: 0xbe}
var clearColExpect = f32color.NRGBAToRGBA(clearCol)
var (
clearCol = color.NRGBA{A: 0xff, R: 0xde, G: 0xad, B: 0xbe}
clearColExpect = f32color.NRGBAToRGBA(clearCol)
)
func TestFramebufferClear(t *testing.T) {
b := newDriver(t)
@@ -202,5 +204,5 @@ func saveImage(file string, img image.Image) error {
if err := png.Encode(&buf, img); err != nil {
return err
}
return os.WriteFile(file, buf.Bytes(), 0666)
return os.WriteFile(file, buf.Bytes(), 0o666)
}
-1
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build linux || freebsd || openbsd
// +build linux freebsd openbsd
package headless
+3 -8
View File
@@ -152,9 +152,7 @@ func newDirect3D11Device(api driver.Direct3D11) (driver.Device, error) {
}
func (b *Backend) BeginFrame(target driver.RenderTarget, clear bool, viewport image.Point) driver.Texture {
var (
renderTarget *d3d11.RenderTargetView
)
var renderTarget *d3d11.RenderTargetView
if target != nil {
switch t := target.(type) {
case driver.Direct3D11RenderTarget:
@@ -229,10 +227,7 @@ func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, min
// Flags required by ID3D11DeviceContext::GenerateMips.
bindFlags |= d3d11.BIND_SHADER_RESOURCE | d3d11.BIND_RENDER_TARGET
miscFlags |= d3d11.RESOURCE_MISC_GENERATE_MIPS
dim := width
if height > dim {
dim = height
}
dim := max(height, width)
log2 := 32 - bits.LeadingZeros32(uint32(dim)) - 1
nmipmaps = log2 + 1
}
@@ -802,7 +797,7 @@ func (t *Texture) ReadPixels(src image.Rectangle, pixels []byte, stride int) err
mapSize := dstPitch * h
data := sliceOf(resMap.PData, mapSize)
width := w * 4
for r := 0; r < h; r++ {
for r := range h {
pixels := pixels[r*srcPitch:]
copy(pixels[:width], data[r*dstPitch:])
}
+5 -3
View File
@@ -96,8 +96,10 @@ type BlendFactor uint8
type Topology uint8
type TextureFilter uint8
type TextureFormat uint8
type (
TextureFilter uint8
TextureFormat uint8
)
type BufferBinding uint8
@@ -217,7 +219,7 @@ 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 := 0; y < height/2; y++ {
for y := range height / 2 {
y1 := height - y - 1
dest := y1 * stride
src := y * stride
+5 -12
View File
@@ -8,6 +8,7 @@ import (
"image"
"math/bits"
"runtime"
"slices"
"strings"
"time"
"unsafe"
@@ -717,10 +718,7 @@ func (b *Backend) NewTexture(format driver.TextureFormat, width, height int, min
if mipmap {
nmipmaps := 1
if mipmap {
dim := width
if height > dim {
dim = height
}
dim := max(height, width)
log2 := 32 - bits.LeadingZeros32(uint32(dim)) - 1
nmipmaps = log2 + 1
}
@@ -1132,7 +1130,7 @@ func (b *Backend) setupVertexArrays() {
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 := 0; i < max; i++ {
for i := range max {
b.glstate.setVertexAttribArray(b.funcs, i, enabled[i])
}
}
@@ -1175,7 +1173,7 @@ func (t *texture) ReadPixels(src image.Rectangle, pixels []byte, stride int) err
} 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 := 0; y < h; y++ {
for y := range h {
copy(pixels[y*stride:], tmp[y*w*4:])
}
}
@@ -1357,12 +1355,7 @@ func alphaTripleFor(ver [2]int) textureTriple {
}
func hasExtension(exts []string, ext string) bool {
for _, e := range exts {
if ext == e {
return true
}
}
return false
return slices.Contains(exts, ext)
}
func firstBufferType(typ driver.BufferBinding) gl.Enum {
+21 -21
View File
@@ -66,8 +66,8 @@ func BenchmarkDrawUICached(b *testing.B) {
defer w.Release()
drawCore(gtx, th)
w.Frame(gtx.Ops)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
w.Frame(gtx.Ops)
}
finishBenchmark(b, w)
@@ -83,12 +83,12 @@ func BenchmarkDrawUI(b *testing.B) {
drawCore(gtx, th)
w.Frame(gtx.Ops)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for i := 0; b.Loop(); i++ {
resetOps(gtx)
off := float32(math.Mod(float64(i)/10, 10))
t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(off, off))).Push(gtx.Ops)
t := op.Affine(f32.AffineId().Offset(f32.Pt(off, off))).Push(gtx.Ops)
drawCore(gtx, th)
@@ -105,12 +105,12 @@ func BenchmarkDrawUITransformed(b *testing.B) {
drawCore(gtx, th)
w.Frame(gtx.Ops)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for i := 0; b.Loop(); i++ {
resetOps(gtx)
angle := float32(math.Mod(float64(i)/1000, 0.05))
a := f32.Affine2D{}.Shear(f32.Point{}, angle, angle).Rotate(f32.Point{}, angle)
a := f32.AffineId().Shear(f32.Point{}, angle, angle).Rotate(f32.Point{}, angle)
t := op.Affine(a).Push(gtx.Ops)
drawCore(gtx, th)
@@ -130,8 +130,8 @@ func Benchmark1000Circles(b *testing.B) {
draw1000Circles(gtx)
w.Frame(gtx.Ops)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
resetOps(gtx)
draw1000Circles(gtx)
w.Frame(gtx.Ops)
@@ -147,8 +147,8 @@ func Benchmark1000CirclesInstanced(b *testing.B) {
draw1000CirclesInstanced(gtx)
w.Frame(gtx.Ops)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
for b.Loop() {
resetOps(gtx)
draw1000CirclesInstanced(gtx)
w.Frame(gtx.Ops)
@@ -158,9 +158,9 @@ func Benchmark1000CirclesInstanced(b *testing.B) {
func draw1000Circles(gtx layout.Context) {
ops := gtx.Ops
for x := 0; x < 100; x++ {
for x := range 100 {
op.Offset(image.Pt(x*10, 0)).Add(ops)
for y := 0; y < 10; y++ {
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),
@@ -179,9 +179,9 @@ func draw1000CirclesInstanced(gtx layout.Context) {
cl.Pop()
c := r.Stop()
for x := 0; x < 100; x++ {
for x := range 100 {
op.Offset(image.Pt(x*10, 0)).Add(ops)
for y := 0; y < 10; y++ {
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)
@@ -204,9 +204,9 @@ func drawIndividualShapes(gtx layout.Context, th *material.Theme) chan op.CallOp
go func() {
ops := &op1
c := op.Record(ops)
for x := 0; x < 9; x++ {
for x := range 9 {
op.Offset(image.Pt(x*50, 0)).Add(ops)
for y := 0; y < 9; y++ {
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),
@@ -233,8 +233,8 @@ func drawShapeInstances(gtx layout.Context, th *material.Theme) chan op.CallOp {
squares.Add(ops)
rad := float32(0)
for x := 0; x < 20; x++ {
for y := 0; y < 20; y++ {
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()
@@ -253,7 +253,7 @@ func drawText(gtx layout.Context, th *material.Theme) chan op.CallOp {
c := op.Record(ops)
txt := material.H6(th, "")
for x := 0; x < 40; x++ {
for x := range 40 {
txt.Text = textRows[x]
t := op.Offset(image.Pt(0, 24*x)).Push(ops)
gtx.Ops = ops
+40 -9
View File
@@ -40,6 +40,25 @@ func TestPaintClippedRect(t *testing.T) {
})
}
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
@@ -132,8 +151,7 @@ func TestTexturedStrokeClipped(t *testing.T) {
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)
}, func(r result) {
})
}, nil)
}
func TestTexturedStroke(t *testing.T) {
@@ -146,8 +164,7 @@ func TestTexturedStroke(t *testing.T) {
}.Op().Push(o).Pop()
defer op.Offset(image.Pt(-10, -10)).Push(o).Pop()
paint.PaintOp{}.Add(o)
}, func(r result) {
})
}, nil)
}
func TestPaintClippedTexture(t *testing.T) {
@@ -178,7 +195,6 @@ func TestStrokedPathZeroWidth(t *testing.T) {
paint.Fill(o, red)
cl.Pop()
}
}, func(r result) {
r.expect(0, 0, transparent)
r.expect(10, 50, colornames.Black)
@@ -252,8 +268,7 @@ func TestPathReuse(t *testing.T) {
stroke := clip.Stroke{Path: spec, Width: 3}.Op().Push(o)
paint.Fill(o, color.NRGBA{B: 0xFF, A: 0xFF})
stroke.Pop()
}, func(r result) {
})
}, nil)
}
func TestPathInterleave(t *testing.T) {
@@ -290,6 +305,22 @@ func TestStrokedRect(t *testing.T) {
Width: 5,
}.Op(),
)
}, func(r result) {
})
}, 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)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 787 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 435 B

+81 -15
View File
@@ -24,7 +24,6 @@ func TestTransformMacro(t *testing.T) {
c := constSqPath()
run(t, func(o *op.Ops) {
// render the first Stacked item
m1 := op.Record(o)
dr := image.Rect(0, 0, 128, 50)
@@ -88,10 +87,10 @@ 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.Affine2D{}.Rotate(f32.Pt(20, 20), math.Pi/4)
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.Affine2D{}.Rotate(f32.Pt(20, 20), -math.Pi/4)
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())
@@ -110,7 +109,7 @@ func TestDeferredPaint(t *testing.T) {
paint.PaintOp{}.Add(o)
cl.Pop()
t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(20, 20))).Push(o)
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)
@@ -120,12 +119,11 @@ func TestDeferredPaint(t *testing.T) {
op.Defer(o, paintMacro)
t.Pop()
defer op.Affine(f32.Affine2D{}.Offset(f32.Pt(10, 10))).Push(o).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)
}, func(r result) {
})
}, nil)
}
func constSqPath() clip.Op {
@@ -143,8 +141,10 @@ func constSqPath() clip.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)
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 {
@@ -260,7 +260,7 @@ func TestLinearGradient(t *testing.T) {
Color2: g.To,
}.Add(ops)
cl := clip.RRect{Rect: gr.Round()}.Push(ops)
t1 := op.Affine(f32.Affine2D{}.Offset(pixelAligned.Min)).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()
@@ -323,7 +323,7 @@ func TestLinearGradientAngled(t *testing.T) {
cl = clip.Rect(image.Rect(0, 64, 64, 128)).Push(ops)
paint.PaintOp{}.Add(ops)
cl.Pop()
}, func(r result) {})
}, nil)
}
func TestZeroImage(t *testing.T) {
@@ -359,6 +359,73 @@ func TestImageRGBA(t *testing.T) {
})
}
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
@@ -416,17 +483,16 @@ func TestGapsInPath(t *testing.T) {
func TestOpacity(t *testing.T) {
run(t, func(ops *op.Ops) {
opc1 := paint.PushOpacity(ops, .3)
// Fill screen to exercize the glClear optimization.
// 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{G: 255, A: 255}, clip.Rect{Min: image.Pt(50+20, 10), Max: image.Pt(50+64, 128)}.Op())
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()
}, func(r result) {
})
}, nil)
}
// lerp calculates linear interpolation with color b and p.
+12 -12
View File
@@ -29,7 +29,7 @@ func TestPaintOffset(t *testing.T) {
func TestPaintRotate(t *testing.T) {
run(t, func(o *op.Ops) {
a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/8)
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) {
@@ -42,7 +42,7 @@ func TestPaintRotate(t *testing.T) {
func TestPaintShear(t *testing.T) {
run(t, func(o *op.Ops) {
a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
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) {
@@ -79,7 +79,7 @@ func TestClipOffset(t *testing.T) {
func TestClipScale(t *testing.T) {
run(t, func(o *op.Ops) {
a := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 2)).Offset(f32.Pt(10, 10))
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())
@@ -93,7 +93,7 @@ func TestClipScale(t *testing.T) {
func TestClipRotate(t *testing.T) {
run(t, func(o *op.Ops) {
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(40, 40), -math.Pi/4)).Push(o).Pop()
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) {
@@ -121,7 +121,7 @@ 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.Affine2D{}.Scale(f32.Point{}, f32.Pt(2, 1))).Push(o).Pop()
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) {
@@ -133,7 +133,7 @@ func TestOffsetScaleTexture(t *testing.T) {
func TestRotateTexture(t *testing.T) {
run(t, func(o *op.Ops) {
squares.Add(o)
a := f32.Affine2D{}.Offset(f32.Pt(30, 30)).Rotate(f32.Pt(40, 40), math.Pi/4)
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)
@@ -146,10 +146,10 @@ func TestRotateTexture(t *testing.T) {
func TestRotateClipTexture(t *testing.T) {
run(t, func(o *op.Ops) {
squares.Add(o)
a := f32.Affine2D{}.Rotate(f32.Pt(40, 40), math.Pi/8)
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.Affine2D{}.Offset(f32.Pt(10, 10))).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) {
@@ -168,7 +168,7 @@ func TestComplicatedTransform(t *testing.T) {
defer clip.RRect{Rect: image.Rect(0, 0, 100, 100), SE: 50, SW: 50, NW: 50, NE: 50}.Push(o).Pop()
a := f32.Affine2D{}.Shear(f32.Point{}, math.Pi/4, 0)
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()
@@ -182,13 +182,13 @@ func TestComplicatedTransform(t *testing.T) {
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.Affine2D{}.Offset(f32.Pt(64, 64))
a := f32.AffineId().Offset(f32.Pt(64, 64))
defer op.Affine(a).Push(o).Pop()
b := f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(8, 8))
b := f32.AffineId().Scale(f32.Point{}, f32.Pt(8, 8))
defer op.Affine(b).Push(o).Pop()
c := f32.Affine2D{}.Offset(f32.Pt(-10, -10)).Scale(f32.Point{}, f32.Pt(0.5, 0.5))
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) {
+9 -7
View File
@@ -49,8 +49,8 @@ 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 := 0; r < 4; r++ {
for c := 0; c < 4; c++ {
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)
}
@@ -78,7 +78,7 @@ func run(t *testing.T, f func(o *op.Ops), c func(r result)) {
var img *image.RGBA
var err error
ops := new(op.Ops)
for i := 0; i < 3; i++ {
for i := range 3 {
ops.Reset()
img, err = drawImage(t, 128, ops, f)
if err != nil {
@@ -90,7 +90,9 @@ func run(t *testing.T, f func(o *op.Ops), c func(r result)) {
name := fmt.Sprintf("%s-%d-bad.png", t.Name(), i)
saveImage(t, name, img)
}
c(result{t: t, img: img})
if c != nil {
c(result{t: t, img: img})
}
}
}
@@ -151,7 +153,7 @@ func verifyRef(t *testing.T, img *image.RGBA, frame int) (ok bool) {
}
path = filepath.Join("refs", path+".png")
if *dumpImages {
if err := os.MkdirAll(filepath.Dir(path), 0766); err != nil {
if err := os.MkdirAll(filepath.Dir(path), 0o766); err != nil {
if !os.IsExist(err) {
t.Error(err)
return
@@ -285,7 +287,7 @@ func saveImage(t testing.TB, file string, img *image.RGBA) {
t.Error(err)
return
}
if err := os.WriteFile(file, buf.Bytes(), 0666); err != nil {
if err := os.WriteFile(file, buf.Bytes(), 0o666); err != nil {
t.Error(err)
return
}
@@ -300,5 +302,5 @@ func newWindow(t testing.TB, width, height int) *headless.Window {
}
func scale(sx, sy float32) op.TransformOp {
return op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(sx, sy)))
return op.Affine(f32.AffineId().Scale(f32.Point{}, f32.Pt(sx, sy)))
}
+2 -2
View File
@@ -862,8 +862,8 @@ func (b *Backend) BindUniforms(buffer driver.Buffer) {
buf := buffer.(*Buffer)
cmdBuf := b.currentCmdBuf()
for _, s := range b.pipe.pushRanges {
off := s.Offset()
vk.CmdPushConstants(cmdBuf, b.pipe.desc.layout, s.StageFlags(), off, buf.store[off:off+s.Size()])
off := vk.PushConstantRangeOffset(s)
vk.CmdPushConstants(cmdBuf, b.pipe.desc.layout, vk.PushConstantRangeStageFlags(s), off, buf.store[off:off+vk.PushConstantRangeSize(s)])
}
}
+2 -2
View File
@@ -10,10 +10,10 @@ import (
func BenchmarkPacker(b *testing.B) {
var p packer
p.maxDims = image.Point{X: 4096, Y: 4096}
for i := 0; i < b.N; i++ {
for i := 0; b.Loop(); i++ {
p.clear()
p.newPage()
for k := 0; k < 500; k++ {
for k := range 500 {
_, ok := p.tryAdd(xy(k))
if !ok {
b.Fatal("add failed", i, k, xy(k))
+11 -5
View File
@@ -30,7 +30,7 @@ type pather struct {
type coverer struct {
ctx driver.Device
pipelines [3]*pipeline
pipelines [2][3]*pipeline
texUniforms *coverTexUniforms
colUniforms *coverColUniforms
linearGradientUniforms *coverLinearGradientUniforms
@@ -150,7 +150,7 @@ func newCoverer(ctx driver.Device) *coverer {
c.texUniforms = new(coverTexUniforms)
c.linearGradientUniforms = new(coverLinearGradientUniforms)
pipelines, err := createColorPrograms(ctx, gio.Shader_cover_vert, gio.Shader_cover_frag,
[3]interface{}{c.colUniforms, c.linearGradientUniforms, c.texUniforms},
[3]any{c.colUniforms, c.linearGradientUniforms, c.texUniforms},
)
if err != nil {
panic(err)
@@ -162,7 +162,7 @@ func newCoverer(ctx driver.Device) *coverer {
func newStenciler(ctx driver.Device) *stenciler {
// Allocate a suitably large index buffer for drawing paths.
indices := make([]uint16, pathBatchSize*6)
for i := 0; i < pathBatchSize; i++ {
for i := range pathBatchSize {
i := uint16(i)
indices[i*6+0] = i*4 + 0
indices[i*6+1] = i*4 + 1
@@ -309,7 +309,9 @@ func (p *pather) release() {
func (c *coverer) release() {
for _, p := range c.pipelines {
p.Release()
for _, p := range p {
p.Release()
}
}
}
@@ -405,7 +407,11 @@ func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, c
}
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
c.pipelines[mat].UploadUniforms(c.ctx)
fboIdx := 0
if isFBO {
fboIdx = 1
}
c.pipelines[fboIdx][mat].UploadUniforms(c.ctx)
c.ctx.DrawArrays(0, 4)
}
+2 -2
View File
@@ -10,7 +10,7 @@ import (
)
// Struct returns a byte slice view of a struct.
func Struct(s interface{}) []byte {
func Struct(s any) []byte {
v := reflect.ValueOf(s)
sz := int(v.Elem().Type().Size())
return unsafe.Slice((*byte)(unsafe.Pointer(v.Pointer())), sz)
@@ -27,7 +27,7 @@ func Uint32(s []uint32) []byte {
}
// Slice returns a byte slice view of a slice.
func Slice(s interface{}) []byte {
func Slice(s any) []byte {
v := reflect.ValueOf(s)
first := v.Index(0)
sz := int(first.Type().Size())
+1 -1
View File
@@ -34,7 +34,7 @@ func Parse() {
}
print := false
silent := false
for _, part := range strings.Split(val, ",") {
for part := range strings.SplitSeq(val, ",") {
switch part {
case textSubsystem:
Text.Store(true)
+6 -13
View File
@@ -9,16 +9,16 @@ import (
"errors"
"fmt"
"runtime"
"slices"
"strings"
"gioui.org/gpu"
)
type Context struct {
disp _EGLDisplay
eglCtx *eglContext
eglSurf _EGLSurface
width, height int
disp _EGLDisplay
eglCtx *eglContext
eglSurf _EGLSurface
}
type eglContext struct {
@@ -121,11 +121,9 @@ func (c *Context) VisualID() int {
return c.eglCtx.visualID
}
func (c *Context) CreateSurface(win NativeWindowType, width, height int) error {
func (c *Context) CreateSurface(win NativeWindowType) error {
eglSurf, err := createSurface(c.disp, c.eglCtx, win)
c.eglSurf = eglSurf
c.width = width
c.height = height
return err
}
@@ -157,12 +155,7 @@ func (c *Context) EnableVSync(enable bool) {
}
func hasExtension(exts []string, ext string) bool {
for _, e := range exts {
if ext == e {
return true
}
}
return false
return slices.Contains(exts, ext)
}
func createContext(disp _EGLDisplay) (*eglContext, error) {
+51 -33
View File
@@ -9,8 +9,6 @@ import (
"unsafe"
syscall "golang.org/x/sys/windows"
"gioui.org/internal/gl"
)
type (
@@ -24,51 +22,71 @@ type (
)
var (
libEGL = syscall.NewLazyDLL("libEGL.dll")
_eglChooseConfig = libEGL.NewProc("eglChooseConfig")
_eglCreateContext = libEGL.NewProc("eglCreateContext")
_eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface")
_eglDestroyContext = libEGL.NewProc("eglDestroyContext")
_eglDestroySurface = libEGL.NewProc("eglDestroySurface")
_eglGetConfigAttrib = libEGL.NewProc("eglGetConfigAttrib")
_eglGetDisplay = libEGL.NewProc("eglGetDisplay")
_eglGetError = libEGL.NewProc("eglGetError")
_eglInitialize = libEGL.NewProc("eglInitialize")
_eglMakeCurrent = libEGL.NewProc("eglMakeCurrent")
_eglReleaseThread = libEGL.NewProc("eglReleaseThread")
_eglSwapInterval = libEGL.NewProc("eglSwapInterval")
_eglSwapBuffers = libEGL.NewProc("eglSwapBuffers")
_eglTerminate = libEGL.NewProc("eglTerminate")
_eglQueryString = libEGL.NewProc("eglQueryString")
_eglWaitClient = libEGL.NewProc("eglWaitClient")
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
var loadOnce = sync.OnceValue(loadDLLs)
func loadEGL() error {
var err error
loadOnce.Do(func() {
err = loadDLLs()
})
return err
return loadOnce()
}
func loadDLLs() error {
if err := loadDLL(libEGL, "libEGL.dll"); err != nil {
if err := loadDLL(&libEGL, "libEGL.dll"); err != nil {
return err
}
if err := loadDLL(gl.LibGLESv2, "libGLESv2.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,
}
// d3dcompiler_47.dll is needed internally for shader compilation to function.
return loadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"), "d3dcompiler_47.dll")
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.LazyDLL, name string) error {
err := dll.Load()
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
}
@@ -164,6 +182,6 @@ func eglWaitClient() bool {
// issue34474KeepAlive calls runtime.KeepAlive as a
// workaround for golang.org/issue/34474.
func issue34474KeepAlive(v interface{}) {
func issue34474KeepAlive(v any) {
runtime.KeepAlive(v)
}
+2
View File
@@ -19,6 +19,8 @@ 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 {
+2 -2
View File
@@ -16,7 +16,7 @@ func main() {
flag.Parse()
var b bytes.Buffer
printf := func(content string, args ...interface{}) {
printf := func(content string, args ...any) {
fmt.Fprintf(&b, content, args...)
}
@@ -42,7 +42,7 @@ func main() {
panic(err)
}
err = os.WriteFile(*out, data, 0755)
err = os.WriteFile(*out, data, 0o755)
if err != nil {
panic(err)
}
+3 -3
View File
@@ -41,17 +41,17 @@ var sink RGBA
func BenchmarkLinearFromSRGB(b *testing.B) {
b.Run("opaque", func(b *testing.B) {
for i := 0; i < b.N; i++ {
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; i < b.N; i++ {
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; i < b.N; i++ {
for i := 0; b.Loop(); i++ {
sink = LinearFromSRGB(color.NRGBA{R: byte(i), G: byte(i >> 8), B: byte(i >> 16), A: 0x00})
}
})
+15 -15
View File
@@ -90,7 +90,7 @@ func (e *Extrapolation) Estimate() Estimate {
first := e.get(0)
t := first.t
// Walk backwards collecting samples.
for i := 0; i < len(e.samples); i++ {
for i := range e.samples {
p := e.get(-i)
age := first.t - p.t
if age >= maxAge || t-p.t >= maxSampleGap {
@@ -172,9 +172,9 @@ func decomposeQR(A *matrix) (*matrix, *matrix, bool) {
// https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process
Q := newMatrix(A.rows, A.cols) // Column-major.
Rt := newMatrix(A.rows, A.rows) // R transposed, row-major.
for i := 0; i < Q.rows; i++ {
for i := range Q.rows {
// Copy A column.
for j := 0; j < Q.cols; j++ {
for j := range Q.cols {
Q.set(i, j, A.get(i, j))
}
// Subtract projections. Note that int the projection
@@ -184,9 +184,9 @@ func decomposeQR(A *matrix) (*matrix, *matrix, bool) {
// the normalized column e replaces u, where <e, e> = 1:
//
// proje a = <e, a>/<e, e> e = <e, a> e
for j := 0; j < i; j++ {
for j := range i {
d := dot(Q.col(j), Q.col(i))
for k := 0; k < Q.cols; k++ {
for k := range Q.cols {
Q.set(i, k, Q.get(i, k)-d*Q.get(j, k))
}
}
@@ -197,7 +197,7 @@ func decomposeQR(A *matrix) (*matrix, *matrix, bool) {
return nil, nil, false
}
invNorm := 1 / n
for j := 0; j < Q.cols; j++ {
for j := range Q.cols {
Q.set(i, j, Q.get(i, j)*invNorm)
}
// Update Rt.
@@ -261,8 +261,8 @@ func (m *matrix) approxEqual(m2 *matrix) bool {
return false
}
const epsilon = 0.00001
for row := 0; row < m.rows; row++ {
for col := 0; col < m.cols; col++ {
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
@@ -278,8 +278,8 @@ func (m *matrix) transpose() *matrix {
cols: m.rows,
data: make([]float32, len(m.data)),
}
for i := 0; i < m.rows; i++ {
for j := 0; j < m.cols; j++ {
for i := range m.rows {
for j := range m.cols {
t.set(j, i, m.get(i, j))
}
}
@@ -295,10 +295,10 @@ func (m *matrix) mul(m2 *matrix) *matrix {
cols: m2.cols,
data: make([]float32, m.rows*m2.cols),
}
for i := 0; i < mm.rows; i++ {
for j := 0; j < mm.cols; j++ {
for i := range mm.rows {
for j := range mm.cols {
var v float32
for k := 0; k < m.rows; k++ {
for k := range m.rows {
v += m.get(k, j) * m2.get(i, k)
}
mm.set(i, j, v)
@@ -309,8 +309,8 @@ func (m *matrix) mul(m2 *matrix) *matrix {
func (m *matrix) String() string {
var b strings.Builder
for i := 0; i < m.rows; i++ {
for j := 0; j < m.cols; j++ {
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(", ")
+95
View File
@@ -247,9 +247,11 @@ func (f *Functions) getExtension(name string) js.Value {
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))
@@ -257,36 +259,47 @@ func (f *Functions) BeginQuery(target Enum, query Query) {
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))
@@ -297,9 +310,11 @@ func (f *Functions) BufferData(target Enum, size int, usage Enum, data []byte) {
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() {
@@ -308,54 +323,71 @@ func (f *Functions) CheckFramebufferStatus(target Enum) Enum {
}
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))
@@ -363,45 +395,59 @@ func (f *Functions) DeleteQuery(query Query) {
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))
@@ -409,28 +455,36 @@ func (f *Functions) EndQuery(target Enum) {
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
@@ -438,6 +492,7 @@ func (f *Functions) GetFramebufferAttachmentParameteri(target, attachment, pname
}
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() {
@@ -445,6 +500,7 @@ func (f *Functions) GetBinding(pname Enum) Object {
}
return Object(obj)
}
func (f *Functions) GetBindingi(pname Enum, idx int) Object {
obj := f._getIndexedParameter.Invoke(int(pname), idx)
if !obj.Truthy() {
@@ -452,6 +508,7 @@ func (f *Functions) GetBindingi(pname Enum, idx int) Object {
}
return Object(obj)
}
func (f *Functions) GetInteger(pname Enum) int {
if !f.isWebGL2 {
switch pname {
@@ -461,9 +518,11 @@ func (f *Functions) GetInteger(pname Enum) int {
}
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
@@ -472,6 +531,7 @@ func (f *Functions) GetInteger4(pname Enum) [4]int {
}
return res
}
func (f *Functions) GetFloat4(pname Enum) [4]float32 {
arr := f._getParameter.Invoke(int(pname))
var res [4]float32
@@ -480,12 +540,15 @@ func (f *Functions) GetFloat4(pname Enum) [4]float32 {
}
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))))
@@ -493,12 +556,15 @@ func (f *Functions) GetQueryObjectuiv(query Query, pname Enum) uint {
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:
@@ -512,15 +578,19 @@ func (f *Functions) GetString(pname Enum) string {
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() {
@@ -528,9 +598,11 @@ func (f *Functions) GetVertexAttribBinding(index int, pname Enum) 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() {
@@ -541,74 +613,97 @@ func (f *Functions) InvalidateFramebuffer(target, attachment Enum) {
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)
}
+1 -1
View File
@@ -665,7 +665,7 @@ func (f *Functions) load(forceES bool) error {
case runtime.GOOS == "android":
libNames = []string{"libGLESv2.so", "libGLESv3.so"}
default:
libNames = []string{"libGLESv2.so.2"}
libNames = []string{"libGLESv2.so.2", "libGLESv2.so.3.0"}
}
for _, lib := range libNames {
if h := dlopen(lib); h != nil {
+306 -91
View File
@@ -3,103 +3,217 @@
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 (
LibGLESv2 = windows.NewLazyDLL("libGLESv2.dll")
_glActiveTexture = LibGLESv2.NewProc("glActiveTexture")
_glAttachShader = LibGLESv2.NewProc("glAttachShader")
_glBeginQuery = LibGLESv2.NewProc("glBeginQuery")
_glBindAttribLocation = LibGLESv2.NewProc("glBindAttribLocation")
_glBindBuffer = LibGLESv2.NewProc("glBindBuffer")
_glBindBufferBase = LibGLESv2.NewProc("glBindBufferBase")
_glBindFramebuffer = LibGLESv2.NewProc("glBindFramebuffer")
_glBindRenderbuffer = LibGLESv2.NewProc("glBindRenderbuffer")
_glBindTexture = LibGLESv2.NewProc("glBindTexture")
_glBindVertexArray = LibGLESv2.NewProc("glBindVertexArray")
_glBlendEquation = LibGLESv2.NewProc("glBlendEquation")
_glBlendFuncSeparate = LibGLESv2.NewProc("glBlendFuncSeparate")
_glBufferData = LibGLESv2.NewProc("glBufferData")
_glBufferSubData = LibGLESv2.NewProc("glBufferSubData")
_glCheckFramebufferStatus = LibGLESv2.NewProc("glCheckFramebufferStatus")
_glClear = LibGLESv2.NewProc("glClear")
_glClearColor = LibGLESv2.NewProc("glClearColor")
_glClearDepthf = LibGLESv2.NewProc("glClearDepthf")
_glDeleteQueries = LibGLESv2.NewProc("glDeleteQueries")
_glDeleteVertexArrays = LibGLESv2.NewProc("glDeleteVertexArrays")
_glCompileShader = LibGLESv2.NewProc("glCompileShader")
_glCopyTexSubImage2D = LibGLESv2.NewProc("glCopyTexSubImage2D")
_glGenerateMipmap = LibGLESv2.NewProc("glGenerateMipmap")
_glGenBuffers = LibGLESv2.NewProc("glGenBuffers")
_glGenFramebuffers = LibGLESv2.NewProc("glGenFramebuffers")
_glGenVertexArrays = LibGLESv2.NewProc("glGenVertexArrays")
_glGetUniformBlockIndex = LibGLESv2.NewProc("glGetUniformBlockIndex")
_glCreateProgram = LibGLESv2.NewProc("glCreateProgram")
_glGenRenderbuffers = LibGLESv2.NewProc("glGenRenderbuffers")
_glCreateShader = LibGLESv2.NewProc("glCreateShader")
_glGenTextures = LibGLESv2.NewProc("glGenTextures")
_glDeleteBuffers = LibGLESv2.NewProc("glDeleteBuffers")
_glDeleteFramebuffers = LibGLESv2.NewProc("glDeleteFramebuffers")
_glDeleteProgram = LibGLESv2.NewProc("glDeleteProgram")
_glDeleteShader = LibGLESv2.NewProc("glDeleteShader")
_glDeleteRenderbuffers = LibGLESv2.NewProc("glDeleteRenderbuffers")
_glDeleteTextures = LibGLESv2.NewProc("glDeleteTextures")
_glDepthFunc = LibGLESv2.NewProc("glDepthFunc")
_glDepthMask = LibGLESv2.NewProc("glDepthMask")
_glDisableVertexAttribArray = LibGLESv2.NewProc("glDisableVertexAttribArray")
_glDisable = LibGLESv2.NewProc("glDisable")
_glDrawArrays = LibGLESv2.NewProc("glDrawArrays")
_glDrawElements = LibGLESv2.NewProc("glDrawElements")
_glEnable = LibGLESv2.NewProc("glEnable")
_glEnableVertexAttribArray = LibGLESv2.NewProc("glEnableVertexAttribArray")
_glEndQuery = LibGLESv2.NewProc("glEndQuery")
_glFinish = LibGLESv2.NewProc("glFinish")
_glFlush = LibGLESv2.NewProc("glFlush")
_glFramebufferRenderbuffer = LibGLESv2.NewProc("glFramebufferRenderbuffer")
_glFramebufferTexture2D = LibGLESv2.NewProc("glFramebufferTexture2D")
_glGenQueries = LibGLESv2.NewProc("glGenQueries")
_glGetError = LibGLESv2.NewProc("glGetError")
_glGetRenderbufferParameteriv = LibGLESv2.NewProc("glGetRenderbufferParameteriv")
_glGetFloatv = LibGLESv2.NewProc("glGetFloatv")
_glGetFramebufferAttachmentParameteriv = LibGLESv2.NewProc("glGetFramebufferAttachmentParameteriv")
_glGetIntegerv = LibGLESv2.NewProc("glGetIntegerv")
_glGetIntegeri_v = LibGLESv2.NewProc("glGetIntegeri_v")
_glGetProgramiv = LibGLESv2.NewProc("glGetProgramiv")
_glGetProgramInfoLog = LibGLESv2.NewProc("glGetProgramInfoLog")
_glGetQueryObjectuiv = LibGLESv2.NewProc("glGetQueryObjectuiv")
_glGetShaderiv = LibGLESv2.NewProc("glGetShaderiv")
_glGetShaderInfoLog = LibGLESv2.NewProc("glGetShaderInfoLog")
_glGetString = LibGLESv2.NewProc("glGetString")
_glGetUniformLocation = LibGLESv2.NewProc("glGetUniformLocation")
_glGetVertexAttribiv = LibGLESv2.NewProc("glGetVertexAttribiv")
_glGetVertexAttribPointerv = LibGLESv2.NewProc("glGetVertexAttribPointerv")
_glInvalidateFramebuffer = LibGLESv2.NewProc("glInvalidateFramebuffer")
_glIsEnabled = LibGLESv2.NewProc("glIsEnabled")
_glLinkProgram = LibGLESv2.NewProc("glLinkProgram")
_glPixelStorei = LibGLESv2.NewProc("glPixelStorei")
_glReadPixels = LibGLESv2.NewProc("glReadPixels")
_glRenderbufferStorage = LibGLESv2.NewProc("glRenderbufferStorage")
_glScissor = LibGLESv2.NewProc("glScissor")
_glShaderSource = LibGLESv2.NewProc("glShaderSource")
_glTexImage2D = LibGLESv2.NewProc("glTexImage2D")
_glTexStorage2D = LibGLESv2.NewProc("glTexStorage2D")
_glTexSubImage2D = LibGLESv2.NewProc("glTexSubImage2D")
_glTexParameteri = LibGLESv2.NewProc("glTexParameteri")
_glUniformBlockBinding = LibGLESv2.NewProc("glUniformBlockBinding")
_glUniform1f = LibGLESv2.NewProc("glUniform1f")
_glUniform1i = LibGLESv2.NewProc("glUniform1i")
_glUniform2f = LibGLESv2.NewProc("glUniform2f")
_glUniform3f = LibGLESv2.NewProc("glUniform3f")
_glUniform4f = LibGLESv2.NewProc("glUniform4f")
_glUseProgram = LibGLESv2.NewProc("glUseProgram")
_glVertexAttribPointer = LibGLESv2.NewProc("glVertexAttribPointer")
_glViewport = LibGLESv2.NewProc("glViewport")
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 {
@@ -109,57 +223,74 @@ type Functions struct {
uintptrs [100]uintptr
}
type Context interface{}
type Context any
func NewFunctions(ctx Context, forceES bool) (*Functions, error) {
if ctx != nil {
panic("non-nil context")
}
return new(Functions), nil
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 {
@@ -167,6 +298,7 @@ func (c *Functions) BufferData(target Enum, size int, usage Enum, data []byte) {
}
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]
@@ -174,93 +306,118 @@ func (f *Functions) BufferSubData(target Enum, offset int, src []byte) {
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 {
@@ -268,42 +425,55 @@ func (c *Functions) DepthMask(mask bool) {
}
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]
@@ -311,24 +481,30 @@ func (f *Functions) GetUniformBlockIndex(p Program, name string) uint {
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
@@ -337,52 +513,66 @@ func (c *Functions) GetInteger4(pname Enum) [4]int {
}
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]
@@ -390,6 +580,7 @@ func (c *Functions) GetUniformLocation(p Program, name string) Uniform {
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])
@@ -403,6 +594,7 @@ 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 {
@@ -411,77 +603,99 @@ func (c *Functions) InvalidateFramebuffer(target, attachment Enum) {
}
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 {
@@ -489,6 +703,7 @@ func (c *Functions) VertexAttribPointer(dst Attrib, size int, ty Enum, normalize
}
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)
}
@@ -501,6 +716,6 @@ func cString(s string) []byte {
// issue34474KeepAlive calls runtime.KeepAlive as a
// workaround for golang.org/issue/34474.
func issue34474KeepAlive(v interface{}) {
func issue34474KeepAlive(v any) {
runtime.KeepAlive(v)
}
-1
View File
@@ -1,5 +1,4 @@
//go:build !js
// +build !js
package gl
+36 -86
View File
@@ -14,11 +14,11 @@ import (
type Ops struct {
// version is incremented at each Reset.
version int
version uint32
// data contains the serialized operations.
data []byte
// refs hold external references for operations.
refs []interface{}
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
@@ -32,7 +32,7 @@ type Ops struct {
stringRefs []string
// nextStateID is the id allocated for the next
// StateOp.
nextStateID int
nextStateID uint32
// multipOp indicates a multi-op such as clip.Path is being added.
multipOp bool
@@ -55,28 +55,19 @@ const (
TypePopTransform
TypePushOpacity
TypePopOpacity
TypeInvalidate
TypeImage
TypePaint
TypeColor
TypeLinearGradient
TypePass
TypePopPass
TypePointerInput
TypeClipboardRead
TypeClipboardWrite
TypeSource
TypeTarget
TypeOffer
TypeKeyInput
TypeKeyFocus
TypeKeySoftKeyboard
TypeInput
TypeKeyInputHint
TypeSave
TypeLoad
TypeAux
TypeClip
TypePopClip
TypeProfile
TypeCursor
TypePath
TypeStroke
@@ -84,30 +75,28 @@ const (
TypeSemanticDesc
TypeSemanticClass
TypeSemanticSelected
TypeSemanticDisabled
TypeSnippet
TypeSelection
TypeSemanticEnabled
TypeActionInput
)
type StackID struct {
id int
prev int
id uint32
prev uint32
}
// StateOp represents a saved operation snapshot to be restored
// later.
type StateOp struct {
id int
macroID int
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 int
nextID int
currentID uint32
nextID uint32
}
type StackKind uint8
@@ -142,27 +131,19 @@ const (
TypePushOpacityLen = 1 + 4
TypePopOpacityLen = 1
TypeRedrawLen = 1 + 8
TypeImageLen = 1
TypeImageLen = 1 + 1
TypePaintLen = 1
TypeColorLen = 1 + 4
TypeLinearGradientLen = 1 + 8*2 + 4*2
TypePassLen = 1
TypePopPassLen = 1
TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4
TypeClipboardReadLen = 1
TypeClipboardWriteLen = 1
TypeSourceLen = 1
TypeTargetLen = 1
TypeOfferLen = 1
TypeKeyInputLen = 1 + 1
TypeKeyFocusLen = 1 + 1
TypeKeySoftKeyboardLen = 1 + 1
TypeInputLen = 1
TypeKeyInputHintLen = 1 + 1
TypeSaveLen = 1 + 4
TypeLoadLen = 1 + 4
TypeAuxLen = 1
TypeClipLen = 1 + 4*4 + 1 + 1
TypePopClipLen = 1
TypeProfileLen = 1
TypeCursorLen = 2
TypePathLen = 8 + 1
TypeStrokeLen = 1 + 4
@@ -170,9 +151,7 @@ const (
TypeSemanticDescLen = 1
TypeSemanticClassLen = 2
TypeSemanticSelectedLen = 2
TypeSemanticDisabledLen = 2
TypeSnippetLen = 1 + 4 + 4
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
TypeSemanticEnabledLen = 2
TypeActionInputLen = 1 + 1
)
@@ -266,18 +245,18 @@ func AddCall(o *Ops, callOps *Ops, pc PC, end PC) {
bo.PutUint32(data[13:], uint32(end.refs))
}
func PushOp(o *Ops, kind StackKind) (StackID, int) {
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 int) {
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 interface{}) []byte {
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:]
@@ -290,27 +269,27 @@ func Write1String(o *Ops, n int, ref1 string) []byte {
return o.data[len(o.data)-n:]
}
func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte {
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 interface{}, ref2 string) []byte {
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 interface{}) []byte {
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: len(o.data), refs: len(o.refs)}
return PC{data: uint32(len(o.data)), refs: uint32(len(o.refs))}
}
func (s *stack) push() StackID {
@@ -425,28 +404,19 @@ var opProps = [0x100]opProp{
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
TypeInvalidate: {Size: TypeRedrawLen, 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},
TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1},
TypeClipboardRead: {Size: TypeClipboardReadLen, NumRefs: 1},
TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1},
TypeSource: {Size: TypeSourceLen, NumRefs: 2},
TypeTarget: {Size: TypeTargetLen, NumRefs: 2},
TypeOffer: {Size: TypeOfferLen, NumRefs: 3},
TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2},
TypeKeyFocus: {Size: TypeKeyFocusLen, NumRefs: 1},
TypeKeySoftKeyboard: {Size: TypeKeySoftKeyboardLen, 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},
TypeProfile: {Size: TypeProfileLen, NumRefs: 1},
TypeCursor: {Size: TypeCursorLen, NumRefs: 0},
TypePath: {Size: TypePathLen, NumRefs: 0},
TypeStroke: {Size: TypeStrokeLen, NumRefs: 0},
@@ -454,23 +424,21 @@ var opProps = [0x100]opProp{
TypeSemanticDesc: {Size: TypeSemanticDescLen, NumRefs: 1},
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
TypeSemanticDisabled: {Size: TypeSemanticDisabledLen, NumRefs: 0},
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0},
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
}
func (t OpType) props() (size, numRefs int) {
func (t OpType) props() (size, numRefs uint32) {
v := opProps[t]
return int(v.Size), int(v.NumRefs)
return uint32(v.Size), uint32(v.NumRefs)
}
func (t OpType) Size() int {
return int(opProps[t].Size)
func (t OpType) Size() uint32 {
return uint32(opProps[t].Size)
}
func (t OpType) NumRefs() int {
return int(opProps[t].NumRefs)
func (t OpType) NumRefs() uint32 {
return uint32(opProps[t].NumRefs)
}
func (t OpType) String() string {
@@ -489,8 +457,6 @@ func (t OpType) String() string {
return "PushOpacity"
case TypePopOpacity:
return "PopOpacity"
case TypeInvalidate:
return "Invalidate"
case TypeImage:
return "Image"
case TypePaint:
@@ -503,24 +469,10 @@ func (t OpType) String() string {
return "Pass"
case TypePopPass:
return "PopPass"
case TypePointerInput:
return "PointerInput"
case TypeClipboardRead:
return "ClipboardRead"
case TypeClipboardWrite:
return "ClipboardWrite"
case TypeSource:
return "Source"
case TypeTarget:
return "Target"
case TypeOffer:
return "Offer"
case TypeKeyInput:
return "KeyInput"
case TypeKeyFocus:
return "KeyFocus"
case TypeKeySoftKeyboard:
return "KeySoftKeyboard"
case TypeInput:
return "Input"
case TypeKeyInputHint:
return "KeyInputHint"
case TypeSave:
return "Save"
case TypeLoad:
@@ -531,8 +483,6 @@ func (t OpType) String() string {
return "Clip"
case TypePopClip:
return "PopClip"
case TypeProfile:
return "Profile"
case TypeCursor:
return "Cursor"
case TypePath:
+15 -15
View File
@@ -20,14 +20,14 @@ type Reader struct {
type EncodedOp struct {
Key Key
Data []byte
Refs []interface{}
Refs []any
}
// Key is a unique key for a given op.
type Key struct {
ops *Ops
pc int
version int
pc uint32
version uint32
}
// Shadow of op.MacroOp.
@@ -39,8 +39,8 @@ type macroOp struct {
// PC is an instruction counter for an operation list.
type PC struct {
data int
refs int
data uint32
refs uint32
}
type macro struct {
@@ -128,7 +128,7 @@ func (r *Reader) Decode() (EncodedOp, bool) {
if nrefs != 1 {
panic("internal error: unexpected number of macro refs")
}
deferData := Write1(&r.deferOps, n, refs[0])
deferData := Write1(&r.deferOps, int(n), refs[0])
copy(deferData, data)
r.pc.data += n
r.pc.refs += nrefs
@@ -154,8 +154,8 @@ func (r *Reader) Decode() (EncodedOp, bool) {
r.pc = op.endpc
} else {
// Treat an incomplete macro as containing all remaining ops.
r.pc.data = len(r.ops.data)
r.pc.refs = len(r.ops.refs)
r.pc.data = uint32(len(r.ops.data))
r.pc.refs = uint32(len(r.ops.refs))
}
continue
}
@@ -171,11 +171,11 @@ func (op *opMacroDef) decode(data []byte) {
}
bo := binary.LittleEndian
data = data[:TypeMacroLen]
op.endpc.data = int(int32(bo.Uint32(data[1:])))
op.endpc.refs = int(int32(bo.Uint32(data[5:])))
op.endpc.data = bo.Uint32(data[1:])
op.endpc.refs = bo.Uint32(data[5:])
}
func (m *macroOp) decode(data []byte, refs []interface{}) {
func (m *macroOp) decode(data []byte, refs []any) {
if len(data) < TypeCallLen || len(refs) < 1 || OpType(data[0]) != TypeCall {
panic("invalid op")
}
@@ -183,8 +183,8 @@ func (m *macroOp) decode(data []byte, refs []interface{}) {
data = data[:TypeCallLen]
m.ops = refs[0].(*Ops)
m.start.data = int(int32(bo.Uint32(data[1:])))
m.start.refs = int(int32(bo.Uint32(data[5:])))
m.end.data = int(int32(bo.Uint32(data[9:])))
m.end.refs = int(int32(bo.Uint32(data[13:])))
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:])
}
+22 -10
View File
@@ -87,7 +87,7 @@ func (qs *StrokeQuads) lineTo(pt f32.Point) {
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 i := 0; i < segments; i++ {
for range segments {
p0 := qs.pen()
p1 := m.Transform(p0)
p2 := m.Transform(p1)
@@ -327,10 +327,26 @@ func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
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 {
return f32.Point{}
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}
@@ -437,7 +453,6 @@ func flattenQuadBezier(qs StrokeQuads, p0, p1, p2 f32.Point, d, flatness float32
}
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))
@@ -470,7 +485,6 @@ func quadInterp(p, q f32.Point, t float32) f32.Point {
// 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)
@@ -574,12 +588,10 @@ func ArcTransform(p, f1, f2 f32.Point, angle float32) (transform f32.Affine2D, s
}
}
var (
θ = angle / float32(segments)
ref f32.Affine2D // transform from absolute frame to ellipse-based one
rot f32.Affine2D // rotation matrix for each segment
inv f32.Affine2D // transform from ellipse-based frame to absolute one
)
θ := 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),
+106 -2
View File
@@ -9,6 +9,111 @@ import (
"gioui.org/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
@@ -47,12 +152,11 @@ func BenchmarkSplitCubic(b *testing.B) {
}
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 i := 0; i < b.N; i++ {
for b.Loop() {
quads = SplitCubic(from, ctrl0, ctrl1, to, quads[:0])
}
if len(quads) != s.segments {
+7 -7
View File
@@ -385,6 +385,7 @@ static VkResult vkQueuePresentKHR(PFN_vkQueuePresentKHR f, VkQueue queue, const
}
*/
import "C"
import (
"errors"
"fmt"
@@ -1167,7 +1168,6 @@ func CreateFramebuffer(d Device, rp RenderPass, view ImageView, width, height in
return nilFramebuffer, fmt.Errorf("vulkan: vkCreateFramebuffer: %w", err)
}
return fbo, nil
}
func DestroyFramebuffer(d Device, f Framebuffer) {
@@ -1893,27 +1893,27 @@ func BuildWriteDescriptorSetBuffer(set DescriptorSet, binding int, typ Descripto
}
}
func (r PushConstantRange) StageFlags() ShaderStageFlags {
func PushConstantRangeStageFlags(r PushConstantRange) ShaderStageFlags {
return r.stageFlags
}
func (r PushConstantRange) Offset() int {
func PushConstantRangeOffset(r PushConstantRange) int {
return int(r.offset)
}
func (r PushConstantRange) Size() int {
func PushConstantRangeSize(r PushConstantRange) int {
return int(r.size)
}
func (p QueueFamilyProperties) Flags() QueueFlags {
func QueueFamilyPropertiesFlags(p QueueFamilyProperties) QueueFlags {
return p.queueFlags
}
func (c SurfaceCapabilities) MinExtent() image.Point {
func SurfaceCapabilitiesMinExtent(c SurfaceCapabilities) image.Point {
return image.Pt(int(c.minImageExtent.width), int(c.minImageExtent.height))
}
func (c SurfaceCapabilities) MaxExtent() image.Point {
func SurfaceCapabilitiesMaxExtent(c SurfaceCapabilities) image.Point {
return image.Pt(int(c.maxImageExtent.width), int(c.maxImageExtent.height))
}
+1
View File
@@ -17,6 +17,7 @@ static VkResult vkCreateAndroidSurfaceKHR(PFN_vkCreateAndroidSurfaceKHR f, VkIns
}
*/
import "C"
import (
"fmt"
"unsafe"

Some files were not shown because too many files have changed in this diff Show More