631 Commits

Author SHA1 Message Date
qiannian 1a5fa17a39 app: update TopMost comment
TopMost is also supported on Windows.

Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-27 17:50:54 +08:00
qiannian 5191409708 app: [Windows] support TopMost option
Use HWND_TOPMOST and HWND_NOTOPMOST when applying Config.TopMost on Windows.

Leave SWP_NOZORDER set when TopMost hasn't changed, so other Configure calls don't reorder the window.

Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-27 10:38:28 +02:00
Jeff Williams 3eab806940 app: gracefully handle Windows clipboard read errors
Signed-off-by: Jeff Williams <info@anvil-editor.net>
2026-06-26 14:25:53 +02:00
qiannian 30dc7ff294 app: [Windows] don't make raised windows topmost
Use HWND_TOP instead of HWND_TOPMOST in ActionRaise so raising a
window no longer pins it on top.

Signed-off-by: qiannian <qianniancn@gmail.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-06-25 18:53:27 +02:00
Elias Naur 9e18cb93fb ap/internal/windows: change type of HWND_TOPMOST to syscall.Handle
syscall.Handle is equivalent to the C type HWND.

Inspired by a patch by qiannian.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-06-25 11:18:57 +02:00
qiannian e4932e163e widget,io,app: draw IME composition underline
Track IME composition range updates as key.CompositionEvent and route them to
the focused editor. Draw a thin underline under the current composition range
using the editor text material.

Keep the composition range in imeState so it is reset with the rest of the
editor input method state, and normalize it in input.Router before forwarding
CompositionEvent.

Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-17 12:05:40 -04:00
qiannian a8fe27488f app: [Windows] avoid default IME composition window
Mark WM_IME_STARTCOMPOSITION as handled after positioning the IME
composition and candidate windows.

Passing the message on to DefWindowProc can let Windows create its default
composition window, which shows the first composing character below Gio's
caret.

Fixes: https://todo.sr.ht/~eliasnaur/gio/687
Signed-off-by: qiannian <qianniancn@gmail.com>
2026-06-17 09:07:13 +02:00
qiannian 06307313cd 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.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2026-06-17 08:05:23 +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
Elias Naur b66dcc436c app: [macOS] fix transition from maximized to restored
The NSWindow.zoomed property is not reliable when a window is being
constructed. Only call it when necessary.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-02 18:48:18 -05:00
Elias Naur 526db27c75 gpu: fix opacity layer rendering on OpenGL
Fixes: https://todo.sr.ht/~eliasnaur/gio/536
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-02 13:07:15 -05:00
Chris Waldon 27193ae8e8 op/clip: prevent no-op path segments
This commit prevents the insertion of LineTo and QuadTo path segments that have
no visible effect on the path (because the path's pen is already at their end state).
This eliminates whisker artifacts from some stroked paths. Thanks to Morlay for the
bug report leading to this fix.

Fixes: https://todo.sr.ht/~eliasnaur/gio/535
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-09-18 09:28:11 -05:00
Elias Naur 313c488ec3 app: [Windows] remove padding from maximized custom decorated windows
As described in https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543
Windows extends maximized windows outside the visible display. This is
not appropriate for custom decorated windows, so this change implements
a workaround in the handling of WM_NCCALCSIZE.

While here, replace the deltas field from window state to fix issues
when switching between decoration modes.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-09-08 15:22:40 -05:00
Elias Naur f30e936d9a app: [Windows] remove redundant call to SetWindowText
And fix a typo while here.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-09-08 15:12:47 -05:00
Elias Naur ae3bd2a1e1 op/paint: add opacity operation
The new paint.PushOpacity allows for adjusting the opacity of a group
of drawing operations.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-09-08 11:46:17 -05:00
Elias Naur ae43d18ced internal/ops: remove unused TypePushTransform
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-09-07 14:48:11 -05:00
Dominik Honnef b4d93379c4 op: don't allocate for each string reference
When storing a string in an interface value that escapes, Go has to heap
allocate space for the string header, as interface values can only store
pointers. In text-heavy applications, this can lead to hundreds of
allocations per frame due to semantic.LabelOp, the primary user of
string-typed references in ops.

Instead of allocating each string header individually, provide a slice
of strings to store string-typed references in, and store pointers into
this slice as the actual references. This only allocates when resizing
the slice's backing array, and averages out to no allocations, as the
backing array gets reused between calls to Ops.Reset.

We introduce two new functions, Write1String and Write2String, which
make use of this new slice for their last argument. We could've
automated this in the existing Write1 and Write2 methods, but that would
require type assertions on each call, and the vast majority of ops do
not make use of strings.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-09-02 09:02:39 -06:00
Dominik Honnef b9654eb4eb app: support numpad keys in xkb
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-09-01 16:34:16 -06:00
Dominik Honnef 89d20c7d99 app: [macOS] handle mouse dragging with buttons other than the left one
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-08-31 15:52:44 -04:00
Dominik Honnef 14bab8efae app: [macOS] handle middle mouse button correctly
NSView only has events for left, right, and other. Also, the Go side
wasn't actually checking for buttons other than left and right.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-08-31 15:52:39 -04:00
Chris Waldon f437aaf359 io/router: fix semantic area traversal
This commit updates the logic behind SemanticAt to use the same hit area
traversal as normal event routing, which should result in more accurate
results for screen readers trying to resolve widgets that might be partially
obscured by non-semantic content.

While here, I realized that the iteration of hit areas needed to stop at
the first matching semantic area, and I added that capability and updated
the ActionAt logic to leverage it as well.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-31 15:09:05 -04:00
Elias Naur cf5ae4aad9 internal/egl: call eglTerminate after context release
Without eglTerminate, using EGL will crash or report spurious errors after
creating and destroying enough contexts. The test program in #528
takes 5-10 window cycles before errors show up for me.

Fixes: https://todo.sr.ht/~eliasnaur/gio/528
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-23 13:31:31 -06:00
Chris Waldon 8679f49fff text: [API] be absolutely consistent about newline truncation
This commit changes the shaper's behavior when truncating text. Previously, if the final
line allowed by MaxLines ended with a newline, whether or not that newline was truncated
depended upon whether we knew that there was more text after the current paragraph. However,
this makes reasoning about what the shaper will do quite difficult. It seems better to be
consistent. Now we will insert a truncator at the end of the final line if it has a trailing
newline character, regardless of whether it ends the input text.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-22 16:37:24 -06:00
Chris Waldon 83202263b9 text: simplify truncation accounting
This commit reverts the work of several previous attempts to resolve truncation-related
rune accounting problems and adopts a simpler approach. Instead of taking a special codepath
when shaping only a newline, we shape the empty string to get its line metrics. Instead of
modifying the final glyph conditionally to account for runes we never actually shaped, we
track that count on the document type and handle it withing the NextGlyph method.

These changes result in much simpler code, and resolve a real bug. We were accidentally corrupting
cached paragraphs when doing the truncation post-processing in Shaper.layoutText. The modification
made to the final glyph there actually did modify the cached copy, which would then be reused when
that string was shaped again (even if there were a different number of truncated runes after it).

This changeset ensures that the cached copy of a paragraph is never modified.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-22 16:37:20 -06:00
Elias Naur 7fde80e805 app: [Wayland] avoid a race on the send side of the wakeup pipe
Discovered while debugging #528 with -race.

References: https://todo.sr.ht/~eliasnaur/gio/528
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-22 16:33:12 -06:00
Elias Naur e9d0619641 app: [macOS] stop display after any events that may access it
Fixes: https://todo.sr.ht/~eliasnaur/gio/527
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-22 11:57:43 -06:00
Elias Naur 2e524200ab app: [macOS] fix display link callback race
Commit c0c25b777 replaced the synchronizing of the display link callback
from a sync.Map to a cgo.Handle. However, the change didn't take into
account the lifecycle issues: a callback may happen just as the cgo.Handle
is freed, leading to a misuse crash.

This change restores the sync.Map synchronization, which avoids the
lifecycle issue.

Fixes: https://todo.sr.ht/~eliasnaur/gio/526
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-22 11:02:49 -06:00
Chris Waldon cc477e9ca6 app: [Windows] ensure custom window decorations allow resize
This commit fixes a platform inconsistency that prevented custom-decorated windows
from being resizable on edges where their custom decorations placed a draggable
system.ActionInputOp.

The prior behavior always checked for this action type before
checking if the cursor was potentially in a window resize area, which meant that
for windows with material.Decorations, it was impossible to resize those windows
from their top edge. The system.ActionMove handler would always win. This is not
the case on platforms like macOS, so this commit makes the behavior consistent by
prioritizing resize over drag.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-22 09:08:06 -06:00
Veikko Sariola 290b5fe821 widget: click button only if key pressed and released
This commit fixes the non-intuitive behaviour, where hitting return or
space with a button focused, then tabbing to another button and
releasing the key causes the second button to trigger. It feels wrong,
as the "gesture" was never initiated on the second button. The fix makes
widget.Clickable track which key was pressed, in a variable called
pressedKey, and only considers a key release if the released key matches
the pressed key. Finally, if the widget loses focus, pressedKey is
cleared.

Fixes: https://todo.sr.ht/~eliasnaur/gio/525
Signed-off-by: Veikko Sariola <5684185+vsariola@users.noreply.github.com>
2023-08-22 09:07:48 -06:00
Chris Waldon e9cb0b326d io/router: fix system action routing logic
When running ActionAt, the router used to only consider the topmost clip area, even
if that clip area had no input handlers attached whatsoever. This change updates the
logic for that test to use the same traversal as normal event handling, ensuring that
action inputs behave intuitively like any other pointer input area. Included is a test
catching the problematic behavior that prompted this change.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-21 10:44:44 -06:00
Elias Naur 0e77a2b521 app: [Windows] enable drop shadows for custom decorated windows
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-16 15:44:04 -06:00
Elias Naur 63550cc81e app: [Windows] make custom decorated windows behave like regular windows
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-16 15:44:04 -06:00
Chris Waldon 03c21dc1b5 text: add android portability notice to NewShaper
NewShaper cannot be called prior to opening an application window on Android unless
the application does not want system font support. Add a note to this effect to the
constructor.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-12 08:16:28 -06:00
Chris Waldon 05f0dc2513 text: ensure truncated consecutive newlines are handled
This commit ensures that multiple newlines in a row still produce expected
results when occuring within a truncated string. The problem was that we usually
wrap text that is truncated in a way that forces the truncator symbol to appear
at the end *unless* we know we're on the final paragraph of the input text. This
is the right behavior for text that will be displayed, but when shaping a paragraph
containing nothing but a newline, we do not want the truncator symbol in our line.
I simply had to disable the forced truncation contextually to make it work.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-03 15:57:34 -04:00
Chris Waldon c1d975cced go.*: update go-text for empty string fix
This commit updates us to a version of go-text that correctly provides text
dimensions for the empty string when laying it out with width zero. Previously,
zero width would result in text with no height.

Fixes: https://todo.sr.ht/~eliasnaur/gio/518
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-03 07:58:28 -04:00
Chris Waldon 32f15ede7b text: fix additional truncated newline bug
This commit fixes another rune accounting bug that the fuzzer discovered. If we
shaped a space in order to acquire line metrics, but the space itself was truncated,
we would reset the truncated count to zero. This had the side effect of lying to
later logic about whether the truncator run was present at the end of the shaped
text.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-02 12:18:04 -04:00
Chris Waldon d414116990 text: update fuzzer to sometimes truncate
This commit updates the shaper fuzzer to try truncating the text, exposing
new edge cases.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-02 10:44:15 -04:00
Chris Waldon 341978dbcd text: fix zero-width truncated newline rune accounting
This commit fixes another rune accounting issue that only existed when shaping
a solitary newline with zero width while truncating the line.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-02 10:32:14 -04:00
Chris Waldon 80da4d6b02 text: fix EOF detection at newline boundaries
This commit tests and fixes some edge cases that threw off rune accounting
when a newline character was the final rune in the input *and* the text was
being truncated. I imagine they were never previously reported because it's
rare to try to truncate such text.

Thanks to Dominik Honnef for the report and reproducer.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-02 09:44:41 -04:00
Chris Waldon edbf872b44 widget: fix label vertical glyph padding logic
We previously were not handling glyphs that extended vertically beyond the
ascent/descent declared by their font. This is done rarely with text fonts,
but is apparently common among symbol and emoji fonts.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-01 09:09:10 +02:00
Chris Waldon c7c49c3258 text: drop unused line.bounds
This commit removes the logic that calculates the bounding box of a line.
We don't actually use this information anywhere, so computing it is just
a waste of CPU and memory. Widgets arrive at their own bounding boxes from
consuming the glyph stream anyway.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-31 12:07:37 +02:00
Chris Waldon fdd102aaf9 widget: simplify and improve cursor position generation
This commit updates the strategy of our cursor positioning index to eliminate
cursor positions *after* trailing whitespace characters on a line. Eliminating
such cursor positions enables us to collapse trailing whitespace visually without
impacting the editability of text (this will be done in a future commit).

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-31 12:05:07 +02:00
Chris Waldon 8dc03ed655 text,widget: remove fractional line height
The previous logic kept the y offset of a line as a fractional value
until the last possible moment in an effort to be as true to a fractional
line height as possible (minimize the error), but this interacts pathologically
with multi-line text selections, as the selections may have visibly different
gaps between lines. It's better to always shift lines by a fixed quantity of
whole pixels, even if it is technically less accurate to the desired line height.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-31 12:03:18 +02:00
Chris Waldon 1d8b54892a text: commit important fuzz failure test data
This commit adds several notable fuzz test failure cases to our corpus.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-31 12:00:55 +02:00
Chris Waldon 7966832536 go.*: update go-text
This commit updates our version of go-text to pick up important bugfixes to the
line wrapper (fixing some fuzzer-discovered bugs).

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-31 12:00:51 +02:00
Chris Waldon 36a39f7d38 text: handle shaping string containing only newline
This commit ensures that we properly handle the case in which an input string is only
a newline character. We now make a run of text by shaping a space rune and then
drop the glyph/rune data from the space (keeping the line height and such).
The prior behavior would shape zero runes, resulting in no output runs, and thus our
logic for synthesizing a glyph for the newline would never execute while iterating the
runs.

I tried to restructure to instead catch whether there were zero runs after the iteration,
but it came out much uglier and harder to understand that way.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-31 12:00:45 +02:00
Egon Elbre d62057a62e app: fix windows build
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-07-19 13:31:55 +02:00
Chris Waldon ddf770b9d5 widget{,/material}: surface line height manipulation
This commit surfaces fields to manipulate the line height of all label and editor
types. It's unfortunate how this spreads through the API, but I don't see a good
way to eliminate that right now.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-19 10:05:33 +02:00
Chris Waldon acab582487 widget/material: allow configuring default typeface on theme
This commit introduces the material.Theme.Face field, which will automatically
populate the Font.Typeface in every text widget created using a constructor function
in package material.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-19 10:02:18 +02:00
Chris Waldon 6384ab6087 text: add family DSL parser
This commit adds a parser for a simple domain-specific language that
can express a comma-delimited list of font families within a string.

I chose to encode families in this way because the string can be used
as an efficient hash key in a way that a slice of families cannot. Similarly,
using a slice of families would require allocations on the caller side.

The particular format was chosen to allow lists to be written with as little
fanfare as possible. This is why quotation marks are completely optional. It's
easy to read:

  Times New Roman, Georgia, serif

Why force the user to type this (this will parse the same):

  "Times New Roman", "Georgia", "serif"

I've tried to handle edge cases exhaustively. Commas are legal within quotes.
Within a quoted string, you can escape instances of the surrounding quote with
a backslash, and can escape literal backslashes by adding another backslash.

I wrote the lexer/parser by hand, and I hope that they're both easy to understand
and (if need be) extend.

A side effect of the DSL I've chosen (and part of my reasoning for allowing both
single and double quoted strings) is that CSS font-family rules will generally be
valid font family lists in Gio. This means the syntax is already familiar to users
coming from other technologies, and that you can copy from a web-based application
to get a similar font stack in Gio.

Fixes: https://todo.sr.ht/~eliasnaur/gio/317
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-19 10:01:51 +02:00
Chris Waldon 43c47f0883 go.*,text,font{,/opentype},app,gpu,widget{,/material}: [API] load system fonts
This commit updates the text package to be able to load system fonts. As a consequence,
application authors may choose to provide no fonts manually, and it's
also possible that the system provides none (WASM, for instance, currently provides no
system fonts). As such, the text stack needed some minor tweaks to handle this case by
displaying blank spaces where text should be rather than crashing when no faces are
available.

Internally, we are dropping the old method of choosing faces and instead relying solely
on the new font matching logic in go-text. I chose to do this because maintaining two
different sets of logic with a hierarchical relationship proved to be really complex,
and also the go-text logic seems to produce higher-quality choices.

The breaking API change from this commit is the new way of constructing a text shaper
using text.ShaperOptions. Providing no options will result in a shaper that uses solely
system fonts. The various options can be used to disable system font loading and to
provide an already-parsed collection of fonts as per Gio's old API.

The material.NewTheme function now accepts no arguments instead of a font collection.
Users wanting to provide a collection can simply provide a new shaper configured how
they would like:

    theme := material.NewTheme()
	theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))

This commit touches many packages to fix up their construction of text shapers, mostly in
test code. The changes to the tests in package widget deserve special note:
Changing our font resolution logic caused the tofu characters within the
test strings to use a different font's tofu. This isn't a problem, but shifted
the layout of the shaped text a little bit. I've updated the numbers to expect
the new glyph positions.

Fixes: https://todo.sr.ht/~eliasnaur/gio/309
Fixes: https://todo.sr.ht/~eliasnaur/gio/184
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-19 10:01:51 +02:00
Chris Waldon babe7a292b app,internal/debug: define GIODEBUG env var
This commit defines an environment-variable-based debug mechanism allowing
users to toggle various debug features of their applications at runtime. The
only currently supported features are debug logging in the text stack and
suppressing the usage message that would otherwise be printed if you supplied
a malformed GIODEBUG value. The syntax is a comma-delimited list of features
right now. To see the usage, set the variable to the empty string (or any other
unsupported value):

$ GIODEBUG="" go run .

To suppress the usage message, use GIODEBUG=silent. This may be helpful for scripts
trying to activate debug features and inspect their output across versions of Gio
with different debug options available.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-19 09:54:27 +02:00
Chris Waldon 92bc52c25c app: [Android] ensure data dirs are set by window init
This commit alters the android backend to automatically populate some environment
variables as early as possible in application startup. Specifically, this commit
sets the XDG_{CONFIG,CACHE}_HOME environment variables which are necessary for
the text shaper to infer a valid cache file location.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-19 09:52:09 +02:00
Chris Waldon df782ea7c5 go.mod,.builds/*: update to Go 1.19
We only support the most recent two go versions, and using 1.18 prevents use of
atomic.Bool, failing CI for a different patchset of mine.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-19 09:50:34 +02:00
inkeliz 74a87b1092 app/io: [android,js] add password keyboard hint
Fixes: https://todo.sr.ht/~eliasnaur/gio/517

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2023-07-17 22:49:44 +02:00
Chris Waldon 6ea4119a3c text,widget: [API] implement consistent, controllable line height
This commit ensures that any given paragraph of text shaped by Gio will use a single
internal line height. This line height is determined (by default) by the text size,
rather than the fonts involved. This is a breaking change, as previously we would
blindly use the largest line height of any font in a line for that line, leading to
lines within the same paragraph with extremely uneven spacing. This commit also
updates some test expectations in package widget.

I thought pretty hard about how to implement line spacing, and consulted a few sources:

[0] https://www.figma.com/blog/line-height-changes/
[1] https://practicaltypography.com/line-spacing.html
[2] https://developer.mozilla.org/en-US/docs/Web/CSS/line-height

There is no single, universal way to think about line spacing. Fonts internally specify
a line height as the sum of their ascent, descent, and gap, but the line height of two
fonts at the same pixel size (say 20 Sp) can vary wildy (especially across writing systems).
There are two strategies we could pursue to establish the line height of a paragraph of text:

- derive the line height from the fonts involved (our old behavior, and the behavior of
  many word processors)
- derive the line height from the requested text size provided by the user (the behavior of the
  web).

The challenge with the first option is that for a given piece of text in the UI, there can
be a silly number of fonts involved. If a label dispays user-generated content, the user can
put an emoji in it, and emoji fonts have different line heights from latin ones. This can cause
unexpected and nasty layout shift. Gio would previously do exactly this, on a line-by-line basis,
resulting in unevenly spaced lines within a paragraph depending on which fonts were used on
which lines. Choosing one of the fonts and enforcing its line height would make things consistent,
but it isn't clear how to choose that canonical font. There is no 1:1 mapping between the input
text.Font provided in the shaping parameters and a single font.Face. Instead, that mapping depends
upon the runes being shaped.

I think the only sane way to implement the first option would be to synthesize some text in the
provided system.Locale (mapping the language to a script and then generating a rune from that
script), shape that single rune, and then enforce the line height of the resulting face on the
entire paragraph. This would require doing a fair bit more work per paragraph than Gio does today,
so I've opted not to do it.

Instead, the second option allows us to choose a line height based on the size of the text that
the user wants to display. While this can potentially interact poorly with unusually tall fonts,
it means that text will always have a consistent line height.

I've provided two knobs to control line height:

- text.Parameters.LineHeight lets you set a specific height in pixels with a default value of
  text.Parameters.PxPerEm.
- text.Parameters.LineHeightScale applies a scaling factor to the LineHeight, allowing you to
  easily space out text without hard-coding a specific pixel size. The default value here
  (drawn from the recommendations of [1]) is 1.2, which looks pretty good across many fonts.

I've chosen this two-value API because many users will want to set one or the other value. I
considered instead a single value field and a "mode" that would specify how it was used, but
that felt uglier. Also, you *can* set both of these two fields and get predictable results.

I'd like to revisit using the line height of the chosen fonts in the future, but it seems a
little too complex to be worthwhile right now. An interesting option would be making the
select-a-face-using-locale strategy described above an opt-in feature, though some users
might instead want to just use the tallest line height among fonts in use. Something like
this Android API might be appropriate:

[3] https://learn.microsoft.com/en-us/dotnet/api/android.widget.textview.fallbacklinespacing?view=xamarin-android-sdk-13

I'd like to thank Dominik Honnef for some good discussion around this feature, and for pointing
me to some good sources on the subject.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-17 22:33:02 +02:00
Chris Waldon 15031d0b52 font{,/{opentype,gofont}},text: [API] drop monospace font metadata
In the general case, it isn't possible for us to efficiently find system fonts that
are monospace. Fonts don't advertise being monospace frequently, so the only way to
reliably detect it is to check that all glyphs are the same width. This is expensive,
far too much so to be done on every system font when there may be thousands of them.

Other font resolution systems rely upon the user requesting fonts by their family name.
If you want a monospace font, ask for it by name or use a generic name like 'monospace'.
This will be Gio's approach from here on out.

Existing code relying upon setting Variant="Mono" should instead set Typeface="Go Mono"
(for the Go font) or specify another monospace typeface. The generic face "monospace"
will search for one of a set of known monospace fonts that may be available on the system.

Similarly, smallcaps isn't well advertised and users should rely on requesting all-smallcaps
fonts by typeface. To get the Go smallcaps font, use Typeface="Go Smallcaps".

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-17 21:25:10 +02:00
Chris Waldon 5606a961f2 text: fix bitmap y offset computation
This commit fixes a bug that would incorrectly baseline bitmap glyphs text if the line
contained another font with a taller line height. The logic for computing the y offset of
the glyph incorrectly assumed that the Glyph.Ascent was particular to the glyph instead of
the line. I've updated it to use a glyph-specific value.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-17 21:20:36 +02:00
Chris Waldon 6d925a12ff doc: update readme with tag policy
This commit adds a note describing our tagged-version policy to the README.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-01 12:38:39 -04:00
Chris Waldon cc2d2c0abf widget/material: use offsetlast in scroll position calculations
This commit updates the logic that computes scroll viewport coordinates to correctly
consume layout.Position.OffsetLast, which was previously ignored. The impact of ignoring
that field was that dragging on a scroll indicator could sometimes fail to reach the
end of the list.

I've updated the logic to consume that field, which increased the amount of visual
jitter in the position of the scrollbar. I then also added a mechanism for smoothing
the jitter by using both methods of deriving the viewport and synthesizing a viewport
from both.

This new strategy exhibits a lower standard deviation than the other options on each of:

- the length of the scroll indicator
- the change in the start coordinate of the viewport when scrolling smoothly
- the change in the end coordinate of the viewport when scrolling smoothly

Fixes: https://todo.sr.ht/~eliasnaur/gio/504
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-07-01 09:05:06 +02:00
Egon Elbre e5fe3a0732 internal/stroke: optimize SplitCubic
│   sec/op    │   sec/op     vs base               │
SplitCubic/4-10    37.36n ± 0%   36.16n ± 0%  -3.21% (p=0.000 n=10)
SplitCubic/8-10    74.53n ± 0%   72.21n ± 0%  -3.12% (p=0.000 n=10)
SplitCubic/16-10   149.3n ± 1%   144.5n ± 0%  -3.22% (p=0.000 n=10)
SplitCubic/33-10   340.1n ± 0%   334.4n ± 0%  -1.65% (p=0.000 n=10)

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-06-27 12:36:14 +02:00
Egon Elbre 55404aec91 internal/stroke: add BenchmarkSplitCubic
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-06-27 12:34:40 +02:00
Egon Elbre 0edc00a721 internal/stroke: tiny optimization to approxCubeTo
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-06-27 12:34:32 +02:00
Dominik Honnef b183774063 internal/stroke, gpu: reuse slice when splitting cubics
When building GPU vertices from paths, we call stroke.SplitCubic once
per OpCubic. Before this change, each call to stroke.SplitCubic would
allocate a slice, which we would only use to iterate over.

This allocation can be easily avoided by reusing the slice. We can
conveniently store it in gpu.quadSplitter.

In a real application that renders hundreds of paths with tens of
rounded rectangles per path, this saved roughly 4500 allocations (or 1
MB worth) per frame.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-06-19 16:19:07 +02:00
Chris Waldon 90e57c2b18 widget: add method to acquire label shaping metadata
There are many times when an application wants to know metadata about shaped text without
allocating a stateful text widget such as widget.Selectable. This commit introduces widget.TextInfo
and adds an extra LayoutDetailed method to widget.Label returning this struct. Currently
the struct only provides the information necessary to determine whether the text was truncated
(useful for deciding whether a tooltip makes sense), but it can be expanded to include text metrics
in the future for applications which require those.

In the future other text widgets may surface methods of acquiring widget.TextInfos, but the critical
gap in the API is that we can't currently determine whether a stateless label was truncated, so
I'm starting here.

I considered making Label.Layout() always return this, but I didn't want to introduce a breaking
API change yet. I have some other thoughts I want to explore about the label API which might
trigger breaking changes (moving parameters into fields).

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-06-19 16:17:10 +02:00
Egon Elbre 49bb767048 app/internal/xkb: ensure things don't panic
If there's no keyboard attached we don't want to panic
when querying modifiers.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-06-19 16:14:42 +02:00
Lothar May 2327604f58 app: [macOS] consider screen scale when performing system.ActionCenter
Fixes: https://todo.sr.ht/~eliasnaur/gio/505
Signed-off-by: Lothar May <stocks@lotharmay.de>
2023-06-19 16:13:02 +02:00
Chris Waldon 3bb6cca157 app: [API] drop ReadClipboard method
Now that all events are not emitted at the top level, there is no longer
a way to receive the clipboard event generated by this window-global
clipboard read method. As such, this commit drops the useless and confusing
method from the exported API.

Fixes: https://todo.sr.ht/~eliasnaur/gio/501
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-06-11 08:15:27 -06:00
Chris Waldon c6e4eecf21 go.*,text,widget{,/material}: enable configurable line wrapping within words
This commit enables consumers of the text shaper to select a policy for how
line breaking candidates will be chosen. The new default policy can break lines
within "words" (UAX#14 segments) when words do not fit by themselves on a line.
This ensures that text does not horizontally overflow its bounding box unless
the available width is insufficient to display a single UAX#29 grapheme cluster.

Fixes: https://todo.sr.ht/~eliasnaur/gio/467
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-06-07 16:41:14 -06:00
Elias Naur a252394356 widget: update Editor.Delete documentation to refer to graphemes
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-06-07 11:29:36 -06:00
Elias Naur e3ef98dda3 Revert "io/router: try all handlers if a key don't match the focus ancestor tree"
This reverts commit 28c206fc78. The commit
introduced counter-intuitive behaviour as demonstrated by #503. In the
meantime, topmost handlers now receive all unhandled key.Events[0], which
should cover the use-cases that motivated the original commit.

[0] https://gioui.org/commit/0dba85f52e5131c03d903c84355fb90cdb978811

Fixes: https://todo.sr.ht/~eliasnaur/gio/503
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-05-26 17:06:22 -06:00
Chris Waldon 4677b72a4c text: fix over-reading on truncated EOF
When consuming text from an io.Reader, the shaper could hit an EOF when reading the
text, then still try to check whether it was done by calling ReadByte() followed by
UnreadByte(). The ReadByte() would still return EOF, but the UnreadByte() would then
walk the iterator cursor backwards to the final byte of the text. If and only if the
text was being truncated, this unexpected cursor position could cause the shaper to
conclude that there were additional runes that were truncated, and thus the returned
glyph stream would account for too many runes. This commit provides a test and a fix.

Many thanks to Jack Mordaunt for the excellent bug report leading to this fix.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-05-26 16:34:59 -06:00
Elias Naur 8571b25ff7 app: replace uses of Window.dead with Window.destroy
There doesn't seem to be a need for a two-step shutdown sequence, so a
single channel is enough to trigger destruction of the Window.

References: https://todo.sr.ht/~eliasnaur/gio/497
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-05-12 07:54:34 -06:00
Mearaj febadd3145 app: [linux,windows,wasm] scroll horizontally when shift key is pressed
Adds support for horizontal scroll using mousewheel with a shift key.
Support is added for windows, linux (wayland and x11), js (wasm).

Fixes: https://todo.sr.ht/~eliasnaur/gio/398
Signed-off-by: Mearaj <mearajbhagad@gmail.com>
2023-05-06 09:53:50 -06:00
Chris Waldon 59695984e5 layout: add resize helpers for constraints
This commit adds two helper methods to layout.Contraints that make it easier to
manipulate the constraints while keeping their invariants. In particular, code
manually manipulating constraints usually fails to correctly ensure that the
max does not become smaller than the min, the min does not exceed the max, and
that no value goes below zero.

It's quite a few lines to check these invariants yourself in every custom layout,
so I think it makes sense to offer helpers for this.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-05-02 12:33:30 -06:00
Chris Waldon 0e5ec18a82 text: fix 32-bit glyph id packing
This commit fixes a problem in the unpacking of text.GlyphID on 32 bit architectures.
Incorrectly casting to an `int` on those platforms resulted in truncating the faceIndex
to always be zero. To catch mistakes like this in the future, I've added tests for this
problem that should be run by our new 32-bit CI testing.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-29 10:00:49 -06:00
Chris Waldon 2a5f8950fd ci: run tests for 32-bit architectures
This commit introduces a 32-bit test run to our Linux CI in an attempt
to detect architecture dependent bugs earlier. I was forced to install
the i386 packages in a build step becuase they can only be added after
enabling the architecture. Also GOARCH=386 does not support the race
detector, so I'm not running the tests with race detection enabled for
that GOARCH.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-29 10:00:43 -06:00
Ilia Demianenko bcb123a05e app: [Android] Set high refresh rate on startup
Some devices with high refresh rates limit SurfaceView apps to 60hz
and need a specific API call to set it back. Same approach is used by
https://github.com/ajinasokan/flutter_displaymode. The extra work is
skipped on the devices that don't need it.

Signed-off-by: Ilia Demianenko <ilia.demianenko@gmail.com>
2023-04-29 09:48:51 -06:00
Chris Waldon 816bda7ac7 widget: update textIterator docs for accuracy
The previous docs claimed that failing to set a textMaterial would result in
invisible glyphs when in reality it results in using whatever the current paint
material is. This could be the paint material from before laying out the glyphs,
or it could be the material for a bitmap glyph. As such, it's better to say that
the color is undefined.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-27 07:34:31 -06:00
Chris Waldon bba91263b0 text: optimize shaper paragraph decoding
This commit removes some inefficiencies from the pre-shaper-cache processing of
text. The text is no longer decoded into runes prior to being tested against the
cache, and the search for newlines uses slightly more efficient iteration operations
now.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-24 20:33:56 -06:00
Chris Waldon 880cd27f59 app: use more efficient window decoration font load
This commit switches to the new Regular() collection method in gofont,
ensuring that the regular face is only ever loaded once.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-21 13:21:46 -06:00
Chris Waldon 0dfd8c3da6 font/gofont: allow loading just the regular Go font
This commit introduces a special mechanism to load only the regular version
of the Go font. This is useful for Gio to load a font for drawing window
decorations without forcing applications to load every Go font.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-21 13:21:42 -06:00
Chris Waldon ccf24c0bd2 font/opentype: make reusing font.Face efficient and safe
This commit updates the internal representation of a font to separate the
threadsafe and non-threadsafe operations in a way that enures font.Faces can
be shared by all text shapers in an application. This should ensure that applications
only need to parse fonts a single time, saving a great deal of memory for
applications that open many windows (which each need a different text shaper).

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-21 13:21:37 -06:00
Chris Waldon a7c9ca99f3 widget/material: make ScrollBarStyle.MajorMinLen default to FingerSize
Egon pointed out that the current default is unusable on touch screens in Slack, so this
change should hopefully ensure the indicator is interactable on touch devices.

I considered expanding the minor axis dimensions as well, but I don't know what value to
use. The 38DP default would be enormous on non-mobile displays if we made that the default
width.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-18 16:40:39 -06:00
Chris Waldon f77bf9a42c font/opentype: [API] support font collection loading
This commit adds back support for loading font collections, which we
lost when switching to the harfbuzz-based shaper last January. In
addition, this commit takes advantage of our new font loading library's
metadata facilities to automatically construct text.FontFaces for all
fonts within a collection. This is significantly more ergonomic for
users, and can be used to load single fonts with automatic metadata
detection as well.

I've exposed a opentype.Face.Font() method that can be used to get the
font metadata for a given face as well, though you have to type assert to
see it:

var myFace text.Face
if asOpentype, ok := myFace.(opentype.Face); ok {
    myFont := asOpentype.Font()
}

The one problem with this approach is that the font variant field always
be automatically populated. Mono font detection is supported, but
other variants like SmallCaps are more complicated and may need to be
expressed differently in the future (smallcaps is a feature that any font
file can have, not necessarily a separate font file). See this [0] upstream
issue for details.

Additionally, in order to avoid import cycles, I've moved the declarations
of font attributes to package font. You can fix your code automatically to
refer to the new definitions by running the following:

    gofmt -w -r 'text.FontFace -> font.FontFace' .
    gofmt -w -r 'text.Variant -> font.Variant' .
    gofmt -w -r 'text.Style -> font.Style' .
    gofmt -w -r 'text.Typeface -> font.Typeface' .
    gofmt -w -r 'text.Font -> font.Font' .
    gofmt -w -r 'text.Regular -> font.Regular' .
    gofmt -w -r 'text.Italic -> font.Italic' .
    gofmt -w -r 'text.Thin -> font.Thin' .
    gofmt -w -r 'text.ExtraLight -> font.ExtraLight' .
    gofmt -w -r 'text.Light -> font.Light' .
    gofmt -w -r 'text.Normal -> font.Normal' .
    gofmt -w -r 'text.Medium -> font.Medium' .
    gofmt -w -r 'text.SemiBold -> font.SemiBold' .
    gofmt -w -r 'text.Bold -> font.Bold' .
    gofmt -w -r 'text.ExtraBold -> font.ExtraBold' .
    gofmt -w -r 'text.Black -> font.Black' .
    gofmt -w -r 'text.Hairline -> font.Thin' .
    gofmt -w -r 'text.UltraLight -> font.ExtraLight' .
    gofmt -w -r 'text.DemiBold -> font.SemiBold' .
    gofmt -w -r 'text.UltraBold -> font.ExtraBold' .
    gofmt -w -r 'text.Heavy -> font.Black' .
    gofmt -w -r 'text.ExtraBlack -> font.Black+50' .
    gofmt -w -r 'text.UltraBlack -> font.ExtraBlack' .

Make sure each affected file imports gioui.org/font.

[0] https://github.com/go-text/typesetting/issues/57

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-18 16:22:48 -06:00
Dominik Honnef cda73efa6a app: [Windows] include keyboard modifiers in move, drag, and scroll pointer events
This matches the behavior on Linux and macOS.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-04-18 16:16:09 -06:00
Dominik Honnef b6e0376ad2 io/semantic: avoid unnecessary pointer indirection
Putting a string in an interface value has to (normally) heap allocate
the string header and string. However, putting the address of a local
string variable in an interface value has the same effect, as this
causes the local variable to escape to the heap.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-04-14 16:30:51 -06:00
Chris Waldon 6937a5dd1f go.*,text: update go-text to pick up font transformation caching
This commit picks up improvements in upstream go-text that (among other things)
allow the shaper to reuse a lot of information when shaping the same font face
multiple times (using an LRU cache to keep that information available). I've
tried to pick a reasonable default LRU size of 32 faces.

My simple benchmarks indicate a definitive performance gain and reduction in
memory use across the board, which is especially noticable for complex fonts
like arabic and emoji.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-05 11:32:45 -06:00
Chris Waldon 73787b8478 text,widget: minimize loss of positional precision in shaping
This commit combs through the logic of computing glyph sizes and positions,
attempting to remove all unnecessary rounding and truncation. This is in
an effort to help text display consistently when different-length strings
are displayed near one another.

The specific problem prompting this change was end-aligned text stacked in
rows with a common suffix. If the rows displayed different values, they
would shape such that those final glyphs were at different fractional x
coordinates, and then they would be aligned with rounding that could display
them at different x positions in spite of the fact that both suffixes are
the same glyphs.

By removing rounding from Alignment.Align, the largest problem is fixed, but
I'm also removing other unnecessary loss of precision that can circumstantially
contribute to this sort of visual issue.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-05 11:32:35 -06:00
Elias Naur c0d3f67b04 app: really do deliver top-level key events
This is a fixup of 0dba85f52e.
See discussion at

https://lists.sr.ht/~eliasnaur/gio-patches/%3C20230201160926.240528-1-alessandro.arzilli%40gmail.com%3E#%3C20230404144001.n2pndo5r5aqk46gq@x1%3E

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-04-04 09:05:18 -06:00
Dominik Honnef ad3db5212d app: [Wayland] avoid stuck primary button when invoking window management
Clicking on the window border or the title bar initiates resizing and
moving of the window respectively. This commit fixes a bug where this
would cause a stuck pressed primary button, as we won't receive a
release event. The fix is to only update the set of pressed buttons
after we've decided not to invoke window management.

This fixes a regression introduced by
2957d007a2.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-04-04 06:55:08 -06:00
Chris Waldon e768fe347a widget/material: export LabelStyle.Shaper and document fields
We panic when someone constructs a literal LabelStyle because they cannot possibly
populate the shaper field. The resulting error is cryptic, and unusual within Gio
because most style types are safe to construct literally. This commit enables
creating literal LabelStyles by exporting the Shaper field, and also documents
the purposes of all of the fields.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-04-01 07:50:47 -06:00
inkeliz e0ceb9f335 app: [WASM] add ViewEvent
This change adds ViewEvent for JS/WASM, which returns the HTMLElement
which Gio is been rendered, once started.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2023-03-30 12:49:25 -06:00
Chris Waldon d71f170c29 text: truncate multi-paragraph text correctly
This commit fixes a subtle problem when trunating text widgets that contain
multiple newline-delimited paragraphs.

Paragraphs are the unit of text shaping, so we divide the text into paragraphs
and then iterate those paragraphs performing shaping and line wrapping. If we
have a maximum number of lines to fill, we stop iterating paragraphs when we
use all of the available lines. Usually, if we fill all of the lines the text
shaper will insert the truncator symbol. However, if we exactly fill all of the
lines with the end of a paragraph, the line wrapper is able to fill the line
quota without actually truncating any of the text in that paragraph. Thus it
doesn't insert a truncator even though subsequent paragraphs were truncated (it
has no way to know).

To fix this, I've taught the line wrapper about an explicit scenario in which
we always want to show the truncator symbol *if* we hit the line limit, even if
all of the text in the current paragraph fit. I've then plumbed support for
that through our text stack.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-30 12:04:08 -06:00
Chris Waldon 7e8c10927b text,widget{,/material}: [API] move all shaping parameters into text.Parameters
This commit moves the min/max width of shaped text and the text's Locale into
text.Parameters. They were previously passed as separate function parameters to
the shaper, but this made little sense and added visual noise. This is a breaking
change, but only if you previously invoked the shaping API directly.

Callers of text.(*Shaper).LayoutString should change:

    shaper.LayoutString(params, minWidth, maxWidth, locale, "string")

to

    params.MinWidth=minWidth
    params.MaxWidth=maxWidth
    params.Locale=locale
    shaper.LayoutString(params, "string")

Callers of text.(*Shaper).Layout should do likewise.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:35 -06:00
Chris Waldon 9d0a53fc9f widget{,/material}: [API] split interactive and non-interactive text widgets
This commit separates the types for interactive and non-interactive text within
package widget. widget.Selectable is used for all interactive text. widget.Label
is used for all non-interactive text. There is no longer a field on widget.Label
to provide it with a Selectable. If you want selectable text and are not relying
upon the material pacakge API, you need to create widget.Selectables instead of
widget.Labels. The material package's LabelStyle API is unchanged.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:33 -06:00
Chris Waldon 5e6e1217da widget: expose truncation status of Selectable
This commit adds an exported method to enable widgets to detect
when the text displayed by a Selectable has been truncated. This
can be used to implement proper show-full-text-in-an-overlay
behavior in a parent widget. I haven't attempted to implement
that in core yet, as it is a complex feature involving animation
and pointer interaction.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:31 -06:00
Chris Waldon 959f5889a1 go.*,text,widget{,/material}: implement text truncators
This commit adds support for the idea of a text "Truncator", a string
that is shown at the end of truncated text to indicate that it has been
shortened because it would not fit within the requested number of lines.

When specifying a maximum number of lines, a truncator symbol is always
used. If the user does not provide one, the rune `…` is used. This
requirement results in a better user experience and significantly simpler
code, as we can rely upon the presence of one or more truncator glyphs in
the output glyph stream when truncation has occurred.

When interacting with truncated text, the truncator glyphs all act as
a single, indivisible unit. They can be selected or not, and if selected
they act as the entire contents of the truncated portion of the text.
This means that copying all of a truncated label will copy the entire
label text content, with the truncator symbol not appearing at all.

Concretely, the exposed text API now accepts a Truncator string in
text.Parameters, and there is a new glyph flag FlagTruncator which indicates
that the glyph is part of the truncator run. The truncator run will only
have a single FlagClusterBreak (even if the run would usually have many),
and the glyph with both FlagClusterBreak and FlagTruncator will have the
quantity of truncated runes in its Runes field. This necessitated increasing
the size of the Runes field from a byte to an int, as it's theoretically possible
for quite a lot of text to be truncated.

This commit necessarily bumps our go-text/typesetting dependency to the version
exposing truncation in the exported API.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:28 -06:00
Chris Waldon 5c54268d40 widget: [API] implement UAX#29 grapheme clustering in text widgets
This commit teaches the text widgets how to position their cursor according to
grapheme cluster boundaries rather than rune boundaries. While this is more work,
the results better match the expectations of users. A "grapheme cluster" is a
user-perceived character that may be composed of arbitrarily many runes.

I chose to implement this within widgets for two reasons:

- grapheme cluster boundaries would be extremely difficult to encode within the
glyph stream returned by the text shaper
- not all text needs to be segmented, only text that can be interacted with

All mutation operations exposed by widget.Editor now work in terms of grapheme
clusters instead of runes.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:25 -06:00
Chris Waldon 36e768e716 widget: make glyphIndex reusable
This commit allows the glyph index type to be reset and reused, preventing the
reallocation of numerous buffers when indexing glyphs.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:24 -06:00
Chris Waldon 25171df66a text: cache bitmap glyph image operations
This commit adds caching to the process of extracting bitmap images
from glyphs, ensuring that we only do so once for a given glyph so long
as it isn't evicted from our LRU.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:21 -06:00
Chris Waldon 3bdbcab874 go.*,widget: add initial emoji rendering benchmarks
This commit upgrades our version of eliasnaur.com/font to include a color
emoji font and uses that to benchmark displaying large quantities of emoji.
As expected, this is very slow when the strings change frequently, and uses
silly amounts of memory. Future commits will work to improve this.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:18 -06:00
Chris Waldon 6ab3ff40a6 font/opentype,text,widget{,/material}: [API] support bitmap glyph rendering
This commit supports rendering opentype glyphs containing bitmap data instead of
color data. In order to support returning the shaped bitmap glyphs from the Shaper's
Shape() method, it has gained a second return parameter, an op.CallOp. Adding
that CallOp immediately after or immediately before painting the returned path
will display the bitmap glyphs.

The consequences of supporting colored glyphs forced changes upon the widget APIs
for widgets that display text. Previously text always had a fixed paint material,
so we could rely upon the caller setting the material (e.g. adding a paint.ColorOp)
before painting the glyphs and everything would work. Now that we display image-
based glyphs, we end up changing the painting material to an image midway through
displaying text. This is an awkward consequence of how we currently manage the
painting material, and to work around it widgets now accept an op.CallOp that
is expected to set the proper paint material. Text widgets will use that op.CallOp
before painting text (or other paint operations) to ensure that they are painting
with the proper materials.

This, in turn, changed the APIs for laying out widget.Editor, widget.Label, and
widget.Selectable, and eliminated the need for them to accept a callback (the
callback was only really to set the colors). Dropping that callback function
allowed me to consolidate widget.Label to only need one exported Layout method,
and allowed me to unexport the PaintText, PaintCaret, and PaintSelection methods
from widget.Editor and widget.Selectable. Those methods are useless in the public
API now that they don't need to be invoked after applying a color operation.

Callers of the raw text shaper API will need to make the following changes:

- Where before you used:

	var ops *op.Ops // Assume we have an operation list.
	var shaper *text.Shaper // Assume we have a shaper.
	var col color.NRGBA // Assume we have a text color.
	var glyphs []text.Glyph // Assume we have already filled a slice of glyphs.

	shape := shaper.Shape(glyphs)
	paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())

- Now you should do:

	shape, call := shaper.Shape(glyphs)
	paint.FillShape(ops, col, clip.Outline{Path:shape}.Op())
	call.Add(ops)

Callers of the widget.{Label,Selectable,Editor} APIs will need to make the
following changes:

- Where before you used:

	var gtx layout.Context // Assume we have an operation list.
	var shaper *text.Shaper // Assume we have a shaper.
	var textCol color.NRGBA // Assume we have a text color.
	var selectCol color.NRGBA // Assume we have a selection color.
	var ed widget.Editor // Assume we have an editor.
	var sel widget.Selectable // Assume we have a selectable.

	// Lay out an editor.
	ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
		// Paint the editor.
	})
	// Lay out a selectable.
	sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), func(layout.Context) layout.Dimensions {
		// Paint the selectable.
	})
	// Lay out an interactive label.
	widget.Label{}.LayoutSelectable(gtx, shaper, text.Font{}, unit.Sp(30), "hello", func(layout.Context) layout.Dimensions {
		// Paint the label.
	})
	// Lay out a non-interactive label.
	widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello")

- Now you should do:

	// Capture setting the text paint material in a macro.
	textColMacro := op.Record(gtx.Ops)
	paint.ColorOp{Color: textCol}.Add(gtx.Ops)
	textMaterial := textColMacro.Stop()
	// Capture setting the selection paint material in a macro.
	selectColMacro := op.Record(gtx.Ops)
	paint.ColorOp{Color: selectCol}.Add(gtx.Ops)
	selectMaterial := selectColMacro.Stop()

	// Lay out an editor.
	ed.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
	// Lay out a selectable.
	sel.Layout(gtx, shaper, text.Font{}, unit.Sp(30), textMaterial, selectMaterial)
	// Lay out a label (no difference between interactive and non-interactive)
	widget.Label{}.Layout(gtx, shaper, text.Font{}, unit.Sp(30), "hello", textMaterial, selectMaterial)

Callers of the material package API do not need to make any changes.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:15 -06:00
Chris Waldon 47d25c1394 go.*,font/opentype,text: switch to latest go-text/typesetting api
This commit upgrades our go-text version to the latest one which internalizes
harfbuzz and supports text truncators. This allows us to drop our dependency
upon Benoit's textlayout package.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-28 09:25:09 -06:00
Chris Waldon d7b1c7c33b layout: ensure Spacer obeys constraints
This commit ensures that the Spacer type doesn't break layouts
by ignoring when its min constraints require it to be larger or
its max constraints require it to be smaller.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-23 17:08:41 -06:00
Dominik Honnef 51b11486c5 widget: [API] correct default scaling of images
When no scale factor is set, scale by 1.0, mapping one image pixel to
one device-independent pixel. This matches the behavior of CSS and other
frameworks.

The old code attempted to convert to Dp while taking the image's DPI
into account. This was wrong in two ways:

- It assumed that the default display DPI is 160, but this is only true
  for Android. Other platforms use 96, 162, or leave it undefined. Thus
  image.Layout's idea of a dp didn't match that of Gio on most
  platforms.

- It tried to account for image DPI, and assumed a default of 72. This
  was wrong in that DPI in images is merely metadata meant for printing,
  not display. The vast majority of software such as image viewers and
  image editors do not take DPI into account, mapping one image pixel
  either to one physical pixel or to one device-independent pixel. That
  is, users would expect their images to either display 1 to 1, or scaled
  based on PxPerDp, but not scaled based on the image's DPI.

We default to a scale of 1 to stay consistent with other parts of Gio
that scale by default. Users who don't want any scaling can continue to
set the scale to the inverse of PxPerDp.

While we're here we clarify the documentation of the Scale field.

This change is backwards incompatible for users that relied on the
default scale.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-03-23 17:06:43 -06:00
Larry Clapp fa34121f00 text: fix sorting in faceOrderer.sorted
faceOrderer.sorted tried to put the "primary" font first by tweaking the
"less" function in sort.Slice, but it didn't work correctly.

If item i equaled the "primary" font, less() always returned true. This
did not take into account if item j was the "primary" font, in which
case it could easily be sorted differently.

Rather than adding another special case for that, which I couldn't
convince myself was actually correct in every case, I just searched for
the "primary" font and moved it to the front of the slice, and then
omitted the first item of the slice from the rest of the sorting.

Signed-off-by: Larry Clapp <larry@theclapp.org>
2023-03-23 17:02:43 -06:00
Chris Waldon b09ef80d9f widget: ensure proper modifiers on key events
This commit extends the key event handling for text widgets to always check for
appropriate modifier keys. Previously this wasn't necessary, as the text widgets
would only ever receive key events it registered for, but now it may be the top-level
key event handler and thus receive all key events that aren't handled elsewhere.

Fixes: https://todo.sr.ht/~eliasnaur/gio/487
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-23 16:57:52 -06:00
Dominik Honnef 107401cf07 layout: improve documentation for List.ScrollTo and List.ScrollBy
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-03-23 16:44:32 -06:00
Dominik Honnef dc9a4a4009 layout: simplify implementation of List.ScrollTo
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-03-23 16:44:12 -06:00
Serhat Sevki Dincer 4a1962e5e8 text: simplify font weights
Signed-off-by: Serhat Sevki Dincer <jfcgauss@gmail.com>
2023-03-23 16:37:46 -06:00
Serhat Sevki Dincer 35a8231963 text,widget: remove ineffective assignments
Signed-off-by: Serhat Sevki Dincer <jfcgauss@gmail.com>
2023-03-23 16:37:46 -06:00
Serhat Sevki Dincer 39b1158410 app,gpu{,/headless,/internal/rendertest}: replace io/ioutil with io & os
Signed-off-by: Serhat Sevki Dincer <jfcgauss@gmail.com>
2023-03-23 16:37:46 -06:00
Chris Waldon 1210bbb34a text: test maxlines with exported API
This commit changes _how_ the test for line wrapping is implemented to rely on the
exported API rather than internal symbols.

Thanks to https://github.com/gioui/gio/pull/109 for pointing this out.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-03-23 16:29:02 -06:00
Dominik Honnef 5f818bc5e7 widget/material: use more efficient way of scrolling lists
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-02-23 18:43:50 -06:00
Dominik Honnef 8af4472672 layout: add API for efficiently scrolling to and by items
The majority of scrolling happens by manipulating the index of the first
displayed item instead of by just manipulating the offset. This lets us
avoid having to render all items that were scrolled past.

Instead of numbers of items we could've accepted a ratio in [0, 1] to
scroll by or to, to match the data we get from scrollbars. However,
there are more use cases for scrolling by items, such as keyboard
shortcuts, go-to dialogs, etc. And converting from [0, 1] to items is
trivial for the user as long as they know the number of items, and will
usually be handled for them by a theme.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-02-23 18:43:43 -06:00
Elias Naur bb12508a8a go.*: bump golang.org/x/text
Avoids CVE-2022-32149.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-02-23 16:43:53 -06:00
Elias Naur 0dba85f52e io,app: route all unhandled key events to the topmost handler
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-02-06 17:10:44 -06:00
Elias Naur 32c6a9b10d gpu/internal/rendertest: add issue references to broken tests
References: https://todo.sr.ht/~eliasnaur/gio/479
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-02-06 12:08:04 -06:00
Egon Elbre bce4153640 internal/stroke: fix line overlap
When the line overlaps itself backtracking exactly, e.g.

   path.MoveTo(0, 100)
   path.LineTo(100, 0)
   path.LineTo(0, 100)

then acos calculation is relatively unstable. By using atan2 it avoids
some of such problems in the calculation. Additionally, it simpliflies
the round join calculation.

Fixes: https://todo.sr.ht/~eliasnaur/gio/474
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-02-06 12:01:52 -06:00
Egon Elbre 14a33f3cb7 gpu/internal/rendertest: fix alphaClose check
Apparently, alphaClose has been overflowing and giving the wrong answer
for a while and hence some of the tests are broken. I currently disabled
those tests, because I'm not quite sure where and how they broke.

Also, bumped alpha tolerance to 8, to ignore false positives.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-02-06 11:51:02 -06:00
Gordon Klaus db6b4de0f7 widget/material: [API] move widget.Float.{Axis,Invert} into material.SliderStyle
Signed-off-by: Gordon Klaus <gordon.klaus@gmail.com>
2023-01-27 21:04:32 -06:00
Gordon Klaus 22aa00f476 widget/material: add Float.Invert
Setting Float.Invert=true not only inverts the order of values (which was already easily done by swapping min and max), it also draws the widget inverted so that the track is darkened on the opposite side from usual.

This patch also fixes a bug wherein a vertical slider was drawn inverted by default.

Signed-off-by: Gordon Klaus <gordon.klaus@gmail.com>
2023-01-27 21:04:01 -06:00
Elias Naur ac2c284d16 app: [Android] sanitize IME snippet bounds
Fixes: https://todo.sr.ht/~eliasnaur/gio/473
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-01-27 20:36:12 -06:00
Larry Clapp e0cf570339 widget: add a Focus() method to widget.Clickable
Signed-off-by: Larry Clapp <larry@theclapp.org>
2023-01-18 16:28:18 -06:00
Elias Naur af7afea5a3 gpu: don't allocate null materials buffer when running on the CPU
References: https://todo.sr.ht/~eliasnaur/gio/469
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-01-14 09:59:55 -06:00
Chris Waldon 1eb5c7dbcd widget: use caller-provided buffers for reading out text
This commit alters the textView API to give callers the option to provide
their own buffers for reading text. This enables some widget usecases to
be zero-allocation if a widget simply needs to examine the contents of the
text without returning it as a string.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-01-14 09:54:06 -06:00
Chris Waldon 940f0f6021 widget: document MoveWord current limitations
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-01-14 09:53:22 -06:00
Chris Waldon 1c49532447 widget: update ByteOffset method docs
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-01-14 09:53:09 -06:00
Chris Waldon 044390c9df widget: document update and paint methods on textView
This commit updates the textView to better describe the expectations
and behaviors of the Update and Paint* methods.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-01-14 09:52:55 -06:00
Chris Waldon f6d56dba89 widget: drop obsolete comment fragment
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-01-14 09:52:51 -06:00
Elias Naur f8221bb2ab gpu/internal/opengl: don't query FRAMEBUFFER_ATTACHMENT_COLOR_ENCODING on GLES2
It's not supported in OpenGL ES 2.

References: https://todo.sr.ht/~eliasnaur/gio/469
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-01-06 18:51:20 -06:00
Elias Naur 1a84517b12 gpu/internal/opengl: avoid UNPACK_ROW_LENGTH/PACK_ROW_LENGTH on GLES2
Similarly to WebGL1, they're not supported in OpenGL ES 2.0.

References: https://todo.sr.ht/~eliasnaur/gio/469
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-01-06 18:40:22 -06:00
Egon Elbre 827e20d84d gpu: optimize pack.tryAdd
name       old time/op  new time/op  delta
Packer-32   559µs ± 2%   295µs ± 1%  -47.18%  (p=0.008 n=5+5)

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-01-06 18:26:42 -06:00
Egon Elbre 8bc6737dea gpu: optimize encodeQuadTo
name             old time/op  new time/op  delta
EncodeQuadTo-32  35.4ns ± 1%  11.9ns ± 3%  -66.34%  (p=0.008 n=5+5)

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-01-06 18:26:38 -06:00
Egon Elbre c81a1f9671 widget: fix build for go1.17
go.mod specifies 1.18, due to go.mod behavior and to avoid some issues
with updating the dependencies. However, we can still support older go
version, as long as it compiles with the older version.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-01-06 18:26:10 -06:00
Egon Elbre e9bce02b24 unit: add PxToDp and PxToSp
PxToDp and PxToSp are useful when you are trying to calculate
text-size or widget size based on dynamically sized container.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2023-01-01 10:19:50 -06:00
Chris Waldon aa2a948b86 text,widget: [API] drop runereader based shaping API
The io.Reader based API has the potential to be significantly more
efficient, and there are very few users of the runereader API. This
commit simply drops it entirely in favor of the reader API.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-23 09:31:52 -06:00
Chris Waldon dc6fbf07f0 widget: expose text region resolution
This commit adds exported methods to both LabelState and Editor
allowing callers to locate the text regions representing a range
of runes. This can be used to build interactive subregions of text,
like (for instance) hyperlinks.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-23 09:31:48 -06:00
Chris Waldon e98c8955bb widget{,/material}: rebuild label and editor with textView
This commit rebuilds the editor and label types on the common
foundation provided by textView. This enables labels to have
optional state that makes them selectable, and allows the
two widgets to share the code for managing cursor positions,
displaying selections, and soforth. Labels now have an additional
Layout function which can be invoked if they have a Selectable.
It accepts a layout.Widget used to paint their contents. Stateless
labels should still use the old Layout method.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-23 09:31:45 -06:00
Chris Waldon f99aff96ee widget: create standalone textView
This commit adds a standalone state type for manipulating
and displaying text. It reads text from a minimal interface,
shapes it, tracks valid cursor positions, and provides sizing
and scrolling services to higher-level widgets. My long term
goal with these types is to export them to allow non-core widgets
to build atop them, but I've left them private for now.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-23 09:31:40 -06:00
Chris Waldon 5d6cc2892d text: consume io.Reader in shaper
io.Reader is actually a more efficient interface than io.RuneReader,
as we can pull bytes out and check for cache hits without doing
redundant rune<->string conversions. This isn't implemented yet,
however.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-23 09:31:36 -06:00
Chris Waldon 0b456579a9 widget: add ReadOnly mode to editor
This commit provides a new ReadOnly boolean on the editor. If set, the
editor functions as a selectable label. User interaction cannot change
the contents of the editor (though application code can still use the
API).

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-20 11:08:02 -06:00
Chris Waldon c455f0f342 text,widget: test and fix minWidth alignment
This commit unifies and fixes the shaper's handling of the alignment
minimum width. Previously it was only considered when the text was
a single line, but in hindsight that was clearly a mistake. Now the
maximum width of all shaped lines and the minimum width is used to
set the text alignment.

This commit also fixes an index test in package widget that was
relying on the old (incorrect) alignment behavior.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-19 11:17:16 -06:00
Chris Waldon fe5878bc63 widget: track minWidth of editor for alignment
This commit extends the editor to keep track of its own minimum constraint
and to provide that value to the text shaper for the purpose of aligning
text. Without this, the shaper does not know how much of the width of the
editor to use for alignment purposes.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-19 11:17:12 -06:00
Elias Naur 5d1d1df206 text,widget: use != for flag tests
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-12-16 17:32:30 -06:00
Chris Waldon 12da71821a widget: update glyph iteration
This commit updates the textIterator and glyphIndex types to consume
new flag information provided on glyphs. These changes allow widget.Label
and widget.Editor to correctly compute text bounding boxes and to
generate valid cursor positions at the end of text.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-16 17:27:16 -06:00
Chris Waldon 5b40d3cd47 text: provide start of paragraph glyph marker
This commit adds a new flag to glyphs indicating that they are the
beginning of a new paragraph, as well as adding a guarantee that a
glyph with this flag will always follow a glyph with FlagParagraphBreak,
even if a paragraph break is the last rune in the text. This helps
widgets to find the boundaries and positions of text ending with
newlines reliably.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-16 17:27:08 -06:00
Chris Waldon b0483975b7 text: drop unused field on line
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-15 08:41:40 -06:00
Chris Waldon bfb47538aa text: ensure runereader behaves same as string
This commit fixes a subtle discrepancy in the handling of text input
within the shaper. Text provided as an io.RuneReader with a trailing
newline would generate an extra (empty) line of text, whereas the
same input provided as a string would not.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-15 08:34:58 -06:00
Chris Waldon 2db1a7bfb9 go.*,text: implement shaper-driven line truncation
This commit pushes limiting the maximum number of lines of text into
the shaper implementation. This is more efficient than doing it in
widgets, and also opens the door for future use of the shaper to
insert ellipsis and other truncating characters as appropriate.

I realized that we lost the implementation of limiting the number of
lines of text in my text stack overhaul, so this fixes a regression
from that work.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-14 11:44:03 -06:00
Chris Waldon 719278bb36 widget: unify text painting and fix premature termination
This commit unifies all widget text painting to use a single function
and fixes two bugs that could result in visible glyphs failing to be
painted.

The first bug was that we checked whether a particular glyph's
outline was visible within the viewport and terminated iteration the
first time that we found a glyph that wasn't visible. If the very top
of the next line of text was visible within the viewport, taller glyphs
should be painted since part of them is visible. We would stop as soon
as we got to a short glyph, preventing the rest of the line (and any
tall glyphs it contained) from being painted.

I fixed this first problem by using the ascent/descent of the line containing
a glyph to determine whether it's "visible". While this will conclude that
a small glyph is visible when it may be entirely off-screen, the net result
will be that we will paint the entire line containing the glyph rather than
constructing a special version of the line with only the tall glyphs. This
has better path caching performance, as we don't need a bespoke path for when
the line is partially visible.

The second bug was that when the glyph iterator concluded that the
current glyph was out of the viewport, we would immediately terminate
the loop for painting glyphs without painting any buffered glyphs that
had been determined to be visible.

This second bug was easily fixed by ensuring that we always paint all buffered
glyphs when terminating iteration.

As part of this work, I pulled the (fairly complex) logic of buffering and
painting glyphs into the glyph iterator so that label and editor can share
a single implementation.

I was unable to completely encapsulate the array storing buffered glyphs within
the iterator without it being moved to the heap, so the current glyph iteration
API requires the caller to juggle a slice of glyphs. Hopefully someone in
the future can find a structure that the compiler's escape analysis understands.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-14 09:00:31 -06:00
Chris Waldon b7d126e24c font/{gofont,opentype},text,widget{,/material}: [API] add font fallback and bidi support
This commit restructures the entire text shaping stack to enable lines of shaped text to
have non-homogeneous properties like which font face they belong to and which direction
a segment of text is going.

The text package now provides a concrete type text.Shaper which can be used to convert
strings into sequences of renderable text.Glyphs. At a high level, the API is used
like this:

    // Prepare some fonts.
    var collection []text.FontFace
    // Make a shaper with those fonts loaded.
    shaper := text.NewShaper(collection)
    // Shape a string.
    shaper.LayoutString(text.Parameters{
		PxPerEm: fixed.I(12),
    }, 0, 100, system.Locale{}, "Hello")
    // Iterate the glyphs from that string.
    for glyph, ok := shaper.NextGlyph(); ok; glyph, ok = shaper.NextGlyph() {
    	// Convert the glyph data into a path. In real uses, convert batches of glyphs
    	// rather than single glyphs to reduce the number of individual paths and offsets
    	// required to display your text.
    	shape := shaper.Shape([]text.Glyph{glyph})
    	// Offset the glyph to the position it declares within its fields. This will
    	// automatically handle correct bidirectional text glyph positioning.
    	offset := op.Offset(image.Pt(glyph.X.Floor(), int(glyph.Y))).Push(gtx.Ops)
    	// Create a clip area from the shape of the glyph.
    	area := clip.Outline{Path: shape}.Push(gtx.Ops)
    	// Paint whatever the current color is within the glyph's shape.
    	paint.PaintOp{}.Add(gtx.Ops)
    	area.Pop()
        offset.Pop()
    }

This API will transparently handle both font fallback (choosing appropriate fonts
from those loaded when the primary font doesn't contain a required glyph) and
bidirectional text (mixed left-to-right and right-to-left text). Glyphs are
iterated in order of the input runes, not their visual order, but proper use
of the provided offsets will ensure that text always displays correctly.

Thanks to Elias Naur for suggesting this glyph iterator strategy. It let us cut
through a lot of accumulated complexity from trying to match our old text APIs,
meaning that this change actually is a net negative change in lines of code.

This commit consumes the upstream github.com/go-text/typesetting/shaping API
now that my prior work is merged there, removing the need for the font/opentype/internal
package entirely.

As part of my efforts, I fuzzed both the low-level text shaping stack and the
editor widget extensively. I've committed regression tests found that way into
the appropriate testdata files to ensure the fuzzer re-checks them.

Fixes: https://todo.sr.ht/~eliasnaur/gio/425
Fixes: https://todo.sr.ht/~eliasnaur/gio/211
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-12-13 22:06:57 -06:00
Elias Naur 513250122c app: [macOS] defer Window destroy to after window close
The windowWillClose callback is too soon to destroy our Window:
at least draw callbacks may be called after windowWillClose but
before the window is gone. This change moves cleanup to the
viewDidMoveToWindow callback where we're sure the NSView is no longer
active.

Fixes: https://todo.sr.ht/~eliasnaur/gio/466
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-12-13 22:06:57 -06:00
Elias Naur 98f098f53f app: [macOS] properly handle middle mouse button up event
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-12-13 22:06:57 -06:00
Elias Naur eccc94dceb .builds: bump to Go 1.18.9
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-12-13 17:30:31 -06:00
Marko Kungla a22e0f527a app: add app.ID exposed to the platform.
Allow app ID to be set by linger flag -X gioui.org/app.ID=%s so that wayland
can group windows, search for ${gioui.org/app.ID}.desktop file and display
application name. e.g. /usr/share/applications/${gioui.org/app.ID}.desktop
~/.local/share/applications/${appID}.desktop.

ID is set by the gogio tool or manually with the -X linker flag.

Signed-off-by: Marko Kungla <marko.kungla@gmail.com>
2022-12-10 12:13:09 -06:00
Marko Kungla 42b2174dec app: wayland force redraw on config change
When applying window config on runtime, it is nessesary
to do full redraw in order to changed config option to
apply correctly. This fixes a bug where e.g window size
change renders next frame in updated dimensions while
native window is not scaled yet since it was waiting
for stage event to apply resize.

Signed-off-by: Marko Kungla <marko.kungla@gmail.com>
2022-12-10 11:58:23 -06:00
Elias Naur dee53b3645 app: fix Windows IME caret positioning
Some IME editors don't send explicit GCS_CURSORPOS messages, in
which case we should assume the cursor moves to the end of the
composition string.

Fixes: https://todo.sr.ht/~eliasnaur/gio/458
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-11-22 07:59:04 -06:00
Chris Waldon 5c84cf7e90 widget: do not allow invalid utf8 in editor
This commit replaces invalid UTF8 codepoints with the replacement character
when they are inserted into the editor. This ensures that the editor never
moves the editing gap to an invalid location and reads its contents.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-11-16 16:22:43 -06:00
Chris Waldon 4f5a6b3212 widget: define text rendering benchmarks
This commit adds a series of benchmarks for text rendering. They are intended
to capture the performance of static and continuously changing text within
labels and editors, and will serve as a baseline to compare the post-bidi
text stack against.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-11-09 08:45:51 -06:00
Chris Waldon b1942f64b0 io/system: implement Stringer on TextDirection
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-11-09 08:45:44 -06:00
Elias Naur c67d8cde4b widget: implement triple click line selection in Editor
Fixes: https://todo.sr.ht/~eliasnaur/gio/455
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-11-07 07:30:04 -06:00
Elias Naur 5c896eabbb gesture,widget: detect multi-click on pointer.Press
References: https://todo.sr.ht/~eliasnaur/gio/455
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-11-07 07:26:19 -06:00
Chris Waldon 9f62230c38 widget: adjust editor tests to new pos iteration
This commit fixes the expectations of our ligature iteration tests to
match the new behavior of the text position iterator. Now the cursor
can reach the position after the final glyph on a line, if that glyph
is not a newline.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-10-22 18:19:56 -06:00
Chris Waldon f7c14e9964 widget: redefine >= and ++ on combinedPos
This commit redefines incrementing a combinedPos to either move a single
rune forward, *or* transition from EOL->BOL, *or* both. This allows traversal
of lines without a trailing newline character to reach the position after the
final glyph of content.

Additionally, this commit updates positionGreaterOrEqual to explicitly handle
hard newlines via special-case logic, allowing lines without a hard newline to
avoid the newline-based short-circuit logic that would prevent them from iteratively
reaching the combinedPos following the final glyph on the line.

Fixes: https://todo.sr.ht/~eliasnaur/gio/400

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-10-22 18:19:51 -06:00
Chris Waldon 2340664570 widget: test and document seekPosition
This commit adds a test for the seekPosition helper, a function which can
be used to move a combinedPos forward through a body of text until it approaches
a position.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-10-22 18:19:46 -06:00
Chris Waldon dee3cc44f9 widget: test positionGreaterOrEqual
This commit adds an exhaustive test case for the positionGreaterOrEqual
helper function that our text widgets use to compare locations within
shaped text.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-10-22 18:19:41 -06:00
Chris Waldon 64db3720fc widget: test and document clusterIndexFor
This commit adds documentation and tests for the clusterIndexFor helper,
making it easier to understand what it does and how to use it safely.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-10-22 18:19:35 -06:00
Chris Waldon b67b322978 widget: define incrementing combinedPos and test
This commit restructures seekPosition from a complex state-manipulating
loop into a simple loop of iteratively applying an increment operation
to the combinedPos. The increment operation itself is now tested, and
much easier to understand.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-10-22 18:19:19 -06:00
Chris Waldon 1be58a2bc4 widget: test firstPos
This commit adds a test to lock in the correct behavior of the
firstPos helper method.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-10-22 18:19:15 -06:00
Chris Waldon b46c0f5907 widget: use reliable text direction checks, not heuristics
This commit switches the way in which the editor and helper functions check
for RTL text from a heuristic to using the actual text direction.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-10-22 18:19:08 -06:00
Elias Naur bebc73db37 gpu: implement automatic mipmaps for images
All GPU APIs except OpenGL ES 2 can generate mipmaps for textures.
This trades 33% more GPU memory use for improved rendering quality
and speed for downscaled images.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-10-22 16:47:00 -06:00
Elias Naur e69ef4f0b4 app: disable OpenGL backend when the noopengl tag is present
The tag `noopengl` is useful for testing the Vulkan backend which
is no longer default.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-10-22 15:10:18 -06:00
Elias Naur b707b199b3 app: make OpenGL default on Android
Like commit dbf6429026, this change
makes the OpenGL backend default for Android.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-10-22 15:10:18 -06:00
Egon Elbre dead6e007f f32: nicer Affine2D string formatting
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-10-11 13:27:57 -06:00
Elias Naur 80196f3c3e op: tolerate incomplete macros
Before this change, a macro not Stop'ed would result in an endless
loop during op decoding.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-10-04 17:11:35 -06:00
Elias Naur 24eb1a4fc5 widget: make the InputOp key.Set empty for unfocused Editors
Fixes: https://todo.sr.ht/~eliasnaur/gio/448
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-09-24 08:48:30 -06:00
Inkeliz 90688fdd17 app,io/system: [API] add StageInactive when window is not in focus
Now, Gio will send one system.StageEvent with system.StageInactive when
the window is not active. It is implemented on macOS and Windows.

This change is not fully backward compatible, if your code compares
the Stage (`stage < system.StageRunning`), you need to consider
the new system.StageInactive.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2022-09-19 11:07:41 -06:00
Inkeliz b1dba5f27d app: remove gofont.Collection by default
This change removes `gofont.Collection()`, which imports multiples fonts and
increase the binary size.

Fixes: https://todo.sr.ht/~eliasnaur/gio/371
Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
2022-09-16 08:06:05 -06:00
Inkeliz 83cb383523 app,internal/gl: [wasm] fix context lost
Before that change, Gio could crash when the WebGL context was lost
unexpectedly. Now, Gio will properly handle such situation and
recreate the buffers/resources when context is restored and will
wait until context is recovered.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
2022-09-15 06:58:27 -06:00
Dominik Honnef e37deed8bb io/router: fix pointer positions of Enter and Leave events for nested areas
Before this change, inverse transformations of pointer positions would
stack up, leading to incorrect positions when an enter or leave event
was delivered to multiple areas.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-09-15 06:41:08 -06:00
Chris Waldon dbf6429026 app,gpu/headless: [linux] make EGL the default backend
This commit switches the priority of EGL and Vulkan so that EGL is always
tried first. This is because our EGL backend performs significantly better
than the Vulkan one, and we want the most performant experience to be the
default.

Our hypothesis is that the EGL backend is benefitting from lots of optimization
within the OpenGL driver that Vulkan drivers expect applications to perform
themselves. Rather than invest a bunch of time optimizing the Vulkan backend
right now, this change lets us focus on other priorities.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-09-08 11:41:35 +02:00
Egon Elbre 276b7eefdd go.mod,go.sum: bump dependencies
Bump golang/x/exp/shiny to use the specific module,
instead of using a single all-encompassing module.
This significantly reduces the number of packages in go.sum.

Unfortunately, this requires us to bump the go.mod language
version to 1.18, otherwise using `go mod tidy` and similar
tools will complain about incompatibility with module lookup
in Go 1.16. The code should still compile with 1.17.

Bump github.com/gioui/uax, which gets rid one additional unneeded
dependency and should also reduce the binary size.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-08-30 15:01:27 +02:00
Chris Waldon 020eb27ff5 widget: add useful state accessors to scrollbar
This commit adds methods to widget.Scrollbar that enable consuming
code to check if the scroll indicator is processing a drag gesture
or if the scroll track is currently being hovered. These accessors
enable scrollbar style types to have enough information to hide the
scroll indicator when it isn't needed, whereas currently they cannot
differentiate between a scrollbar indicator that is being dragged
but hasn't moved since the last frame and a scrollbar indicator that
is not being dragged.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-08-29 15:11:51 +02:00
Elias Naur 911b526dc1 app: [macOS/iOS] remove redundant header include
The redundant include also broke CI.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-29 15:06:12 +02:00
Elias Naur 3b2f2efac7 app: [Wayland] maintain fallback decoration height during maximize
Window.decorations.height is supposed to be a constant during the
lifetime of the window, unlike w.decorations.Config.decoHeight that
varies depending on the decorations state (fallback or custom).
This change makes that so, fixing a problem where the fallback
decorations would fail to offset client content after a maximize
or minimize.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-19 10:51:53 +02:00
Elias Naur 8425d2a6aa layout: ensure Flex{Alignment: Middle} respects minimum constraint
Before this change, the middle alignment would align according to
the widest child. This change aligns according to the widest child
or minimum constraint, whichever is largest.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-16 15:48:46 +02:00
Elias Naur a55065af9c widget: take deleted runes into account when applying Editor.MaxLen
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-08 16:15:21 +02:00
Elias Naur 5fc9312f46 widget: report SubmitEvents for IME newlines in submit mode
Before this change, an IME text edit would always have its newlines
replaced with spaces. However, for Editors where Submit is enabled
we want newlines to result in SubmitEvents.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-08 15:43:36 +02:00
Elias Naur 96d6fd2791 Revert "io/router: [API] don't emit Enter and Leave events for touch input"
This reverts commit cd0c9dab9f. It turns out
that Enter/Leave is important for cancelling press-then-release-outside
for clickables.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-08 14:25:34 +02:00
Inkeliz 65a4366e44 app: [android] fix insets
Previously, the Inset could be report wrongly when the bottom inset
is smaller than the top.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
2022-08-07 23:26:05 +02:00
Elias Naur 61b2e37691 all: format comments with go fmt ./... using Go 1.19
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-06 12:26:03 +02:00
Inkeliz 3e9d4d966c app: [android] use GioView inside FrameLayout
Before that change, on Android, was impossible to overlay GioView with
a custom view. This change adds FrameLayout and renders GioView into
that, allowing to use addView from Android API.

Fixes: https://todo.sr.ht/~eliasnaur/gio/427
Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
2022-08-02 17:04:51 +02:00
Elias Naur dbbae0519e app/permission/wakelock: add package for requesting wake locks
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-01 20:15:44 +02:00
Elias Naur 5326ca5fbe all: add support for macOS to flake.nix
The Nix version of the macOS toolchain has difficulties compiling
Objective-C modules; disable modules instead of figuring out why.
It also doesn't include any frameworks automatically; add them explicitly.

While here, move suppression of OpenGL deprecation to a GL-specific
file.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-08-01 01:33:21 +02:00
Elias Naur f7bc744a24 widget: add Editor.Filter for filtering unwanted characters
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-26 15:22:27 +02:00
Elias Naur 0f51cb9084 app: [macOS] don't miss pointer presses
We used to track the pressed pointer buttons through the global function
[NSEvent pressedMouseButtons]. However, it's possible that at the time a pointer
press event is delivered, the pointer button is up again. To ensure a consistent
view of the pointer press state, track it through the buttonNumber property on
delivered events.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-26 11:47:05 +02:00
Inkeliz fc5689ea44 app: [windows] recover focus on click
Previously, Gio doesn't reclaim the focus when they lose that to a
parent window. In such a case, the child window can steal
keyboard focus, and Gio will never recover it.

Now, Gio will recover the focus when clicked.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
2022-07-26 07:32:52 +02:00
Elias Naur 26e71011f5 widget: don't let unfocused Clickables swallow key presses
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-25 10:57:22 +02:00
Dominik Honnef b67bef3e0d io/pointer: fix order of Cursor comments
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-07-21 10:25:42 +02:00
Dominik Honnef b9416c7c9c f32: fix typo in comment
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-07-21 10:07:52 +02:00
Elias Naur 28c206fc78 io/router: try all handlers if a key don't match the focus ancestor tree
When a key.InputOp is focused, a key.Event is matched to it and its ancestors.
If there is no focus, every handler is matched.
This change always matches to every handler, after checking the focus and
its ancestors.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-20 10:37:28 +02:00
Elias Naur 2993ba1838 app: [Wayland] account for fallback decoration height in window sizes
Pass through a fallback window decoration height to the Wayland backend,
so that it can account for it when determining surface size.

Fixes: https://todo.sr.ht/~eliasnaur/gio/435
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-18 23:17:04 +02:00
Elias Naur 63d2353864 app: ensure no window wakeups are in flight when destroying it
When a window is destroyed, it is no longer valid to call its wakeup
method.

Thanks to Jack Mordaunt for identifying the race.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-18 19:02:07 +02:00
psydvl e711cbc004 internal/vk: fix wayland-client linking
Signed-off-by: psydvl <psydvl@fea.st>
2022-07-18 10:44:47 +02:00
Elias Naur c73125e1c3 app: [X11] send DestroyEvent after ViewEvent{}
Matches the other platforms.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-17 14:52:17 +02:00
Chris Waldon 41de0048db widget: implement editor undo/redo
This commit adds a simple linear-history undo/redo mechanism to
widget.Editor bound to Short-(Shift)-Z as well as tests for this
new feature.

Notes on the implementation:

- using a slice to hold the history does mean that we incur
  allocations as the user types, but I hope that the Go slice
  growth heuristic means that the number of times we pay this
  penalty is very small. We also never shrink the slice in this
  implementation, which ensures that undoing work and then making
  additional modifications is very efficient, but could be framed
  as a memory leak.
- this implementation creates a new history element every time
  we call replace(). This means that, on desktop, it's essentially
  one per rune of input. Users likely want to be able to undo larger
  units of change, so a future improvement could be to coalesce
  changes so long as the selection doesn't change between them.
- I think it's possible to store only one of the Apply/Reverse
  change contents in the history slice, but it's significantly
  more complicated. To implement this, you'd need to add a field
  indicating if the modification represented a forward or backward
  change, and then rewrite the modification's content as you performed
  undo/redo operations.For the time being, I'm not sure it's worth
  this complexity.
- Future work could introduce a limit to the number of history
  entries stored. If we did this, we should also change the
  data structure for storing history. Enforcing such a limit
  using a simple slice like this would be extremely inefficient.
  Perhaps a ring buffer or a linked list would make more sense?
- Applications will likely want to be able to manipulate undo
  history in the future. We may wish to export undo() and redo()
  from the editor. Applications will also likely want a mechanism
  to save the undo history to disk and restore it (implementing
  persistent undo). I'm not sure what the most suitable API for
  that is yet, so I decided not to try to tackle it yet.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-07-17 14:45:59 +02:00
Elias Naur 1d9ab65313 widget: add Editor.MaxLen for limiting the content length og Editor
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-13 18:45:02 +02:00
Elias Naur 48e9cdaffd widget: emit only one ChangeEvent per Editor.Layout
ChangeEvent contains no information, so emitting multiple instances
per layout is pointless.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-13 18:13:59 +02:00
Elias Naur 53da73de35 widget: ensure that Border.Layout dimensions fully contains the border
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-10 14:59:50 +02:00
Elias Naur 162250392b layout: respect minimum constraint size in Flex.Layout
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-08 09:18:13 +02:00
Egon Elbre b4acc239cd widget: fix Enum with Return
The input op started listening to Return, however the check
was looking for Enter.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-07 09:36:43 +02:00
Egon Elbre 0777afb85f gpu: avoid bounds checks in decode
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-07 09:36:26 +02:00
Elias Naur 0057e871d0 io/router: search all key handlers when there is no focus
Fixes: https://todo.sr.ht/~eliasnaur/gio/434
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-02 20:51:26 +02:00
Egon Elbre 6bf5d4dc2d gpu: optimize resourceCache
By keeping all the information in a single map, we avoid multiple
lookups and can switch between frames more easily.

Before ~35us, after ~20us for adding 50 new+old and switching
the frame.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-02 20:12:41 +02:00
Egon Elbre f8f68a4e9f internal/ops: optimize Decode
Using clean struct creation creates a lot of temporary variables in
assembly. Inline the assignments, which generates less code.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-02 20:12:41 +02:00
Egon Elbre 17f604fb50 internal/ops: use single table for OpType
Size and NumRefs are always used together, so consolidate info to
a single table to avoid two separate lookups.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-02 20:12:35 +02:00
Egon Elbre e7dd180447 internal/ops: avoid some bounds checks in decode
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-02 20:11:16 +02:00
Egon Elbre f8efc9c2a6 internal/ops: use lookup table for NumRefs
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-02 20:11:16 +02:00
Egon Elbre d3d2c51717 internal/ops: avoid bounds check in OpType.Size()
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-02 20:06:44 +02:00
Egon Elbre 9de13e37e9 f32: add Affine2D.Split
splitTransform func was creating multiple copies of f32.Affine2D
due to not having access to the internal and passing around non-pointer.

This makes splitTransform from ~8ns to .Split ~2ns.

It seems that the non-pointer receiver was ~0.6ns slower in this case.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-02 07:04:47 +02:00
Egon Elbre 3670f70c01 internal/f32color: optimize LinearFromSRGB
Previously each call was ~100ns, the new implementation is ~1ns.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-07-02 06:57:54 +02:00
Elias Naur 2adf4efcbd app: [Wayland] shrink reize gesture area
As reported by Chris Waldon, the resize area is big enough to make
it hard or impossible to grab and drag any scroll bars.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-01 11:16:28 +02:00
Elias Naur 2957d007a2 app: [Wayland] only start resize gesture on pointer down
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-01 11:15:19 +02:00
Elias Naur cab1184818 app: [Wayland] don't process resize gestures when decorated
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-07-01 08:02:23 +02:00
Chris Waldon a5f8aa35e1 app: [Wayland] scale pointer hotspot coordinates
This commit updates the way that we change cursors so that the
hotspot of the cursor is properly set to surface-local coordinates.
The previous raw hotspot coordinates are relative to the cursor
image buffer data, and do not take the buffer's scaling factor
into account.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-07-01 07:55:37 +02:00
Dominik Honnef 992f568ac7 widget: when clicking on scrollbar, center on that point
Previously, we'd scroll so the new viewportStart corresponded to the
clicked position. This felt okay if clicking above the current
indicator, but felt jarring when clicking below it. Centering gives a
consistent behavior regardless of the scroll direction.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-30 08:01:49 +02:00
Dominik Honnef 6981a88720 widget: move scrollbar indicator if dragging starts outside of it
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-30 08:01:49 +02:00
Dominik Honnef f229601e2d widget: consider size of indicator when limiting scrollbar dragging
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-30 08:01:49 +02:00
Dominik Honnef aea376fbaf widget: clicking on the scrollbar indicator shouldn't jump
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-30 08:01:49 +02:00
Dominik Honnef 8f990a6fdc widget: correctly set s.dragging to false when releasing drag
Before, we would set s.dragging to false on pointer.Release and then
immediately set it back to true because we were processing the event and
saw that s.dragging was false.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-30 08:01:49 +02:00
Dominik Honnef ea37124686 widget: constrain drag offset to [0, 1]
Once the user begins dragging, the cursor can move outside the clip
area (or even the window on at least X11), leading to events with
positions that are either negative, or larger than the clip area.

Negative values outright break the delta tracking and cause the
scrollbar to misbehave. Positive values "only" break the invariant of
Scrollbar.ScrollDistance that the returned value is in the range [-1, 1].

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-30 08:01:49 +02:00
Elias Naur d8766f6d2d app: [macOS] fix Intel build
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-29 22:25:11 +02:00
Elias Naur fa3978e18e app: [macOS] use NSWindow.zoom for Maximized
NSWindow.zoom is what the native window control calls when maximizing.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-29 07:47:45 +02:00
Elias Naur 78d1eab950 app: call driver Perform and Configure after idling
Before this change, Perform and Configure could be called during the
event processing where additional events would be queued. However,
a Maximize animation on macOS works by repeatedly sending draw
requests, and they must not be postponed.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-29 07:35:33 +02:00
Dominik Honnef e21c665e70 text: optimize faceCache.hashGIDs
Use binary.LittleEndian directly instead of going through the
binary.Write indirection. This allows the following optimizations to
occur:

  - We can reuse our own byte slice between iterations
  - We don't have to put g.ID in an interface value
  - h doesn't escape
  - PutUint32 gets inlined

On top of that, the argument to maphash.Hash.Write doesn't escape, so b
doesn't move to the heap.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2022-06-28 18:33:31 +02:00
Elias Naur 4d593927ae app: [Windows] fix window state update after window restore
This patch ensures a correct transition from Minimized to Windowed.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-28 18:32:45 +02:00
Elias Naur 5dc8e0e39d app: [Windows] always send ConfigEvent after Configure
This case was missed in 69e4a3cff3.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-28 18:32:45 +02:00
btop 29f4a1d07b app: fix iOS build
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-28 18:32:33 +02:00
Elias Naur b82b9b258a layout: truncate negative List.Position.First positions to 0
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-27 16:58:32 +02:00
Elias Naur dab796808a internal/gl: avoid excessive Cgo pointer checks
As suggested by Egon Elbre, passing a large struct of function pointers
forces Cgo checks on all of the pointer on every Cgo call. This
change instead passes only the relevant function pointer.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-27 11:08:19 +02:00
Elias Naur fa538f219f app: remove ackEvent, tighten error check
ackEvent is not necessary, a nil event.Event doesn't allocate.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-26 10:14:02 +02:00
Elias Naur 546d971e49 app: [Wayland] ensure monitor scale changes propagate to active windows
This broke during an earlier refactor.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-26 09:18:02 +02:00
Chris Waldon 9151009b2a app: [Wayland] respect XCURSOR_* environment variables
This commit adds support for the commonly-used XCURSOR_THEME and
XCURSOR_SIZE environment variables. Wayland lacks a protocol-level
way to standardize cursor size right now, but these variables are
used consistently by many applications and compositors. Many users
(including me) will find that their environment is already configuring
these for them, and will get consistent cursor sizing for free.

I explored a lot of ways to tackle this, but it looks like nobody has
ever gotten around to implementing the cursor theme protocol discussed
here:

https://wayland-devel.freedesktop.narkive.com/VuMSOO55/possible-wayland-extension-to-publish-mouse-pointer-size

Given that it doesn't seem to be resolved anytime soon, supporting a
widely-used convention to tweak these things seems reasonable to me.

I say that it fixes issue 382 because it enables the user to make
Gio's cursor size match the rest of their system.

Fixes: https://todo.sr.ht/~eliasnaur/gio/382
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-06-26 09:13:43 +02:00
Chris Waldon 414a91c49e app: [Wayland] use HiDPI cursor on HiDPI screen
This commit scales both the loaded cursor theme and the cursor
surface appropriately so that the cursor image is not blurry on
HiDPI screens.

References: https://todo.sr.ht/~eliasnaur/gio/382
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-06-26 09:13:43 +02:00
Elias Naur 3f38e67ce0 io/system: add ActionInputOp to register window move gesture areas
The app.Window.Perform(ActionMove) is the wrong abstraction for
initiating a move gesture: Windows needs to know the move gesture
area at pointer move, and macOS needs to know the pointer button
down event that triggers the move gesture. This change replaces
Perform(ActionMove) with a new system.ActionInputOp that marks an
area movable.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 19:40:46 +02:00
Elias Naur b53cdfef8d io/system: remove resize actions
Allowing clients to initiate resize gestures is a waste: macOS
doesn't support them, and the only reason we added them was to
implement client-side decorations for Wayland. Now all desktop
platforms implement resize gestures as needed, and we no longer
need the system.Action actions.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur b34dc63531 app: [Wayland] hard-code border resize gestures
We're about to remove the system.Action machinery for initiating resize
gestures. This is the Wayland implementation that hard-codes the border
drag gesture for resizing.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur fd3a3eb1b9 app: [Windows] support resize gestures for undecorated windows
We're about to remove the client-controlled system.Action machinery
for resize gestures initiated by the client. This is the replacement
implementation for Windows, where the behaviour is hard-coded to
mimic the decorated automatic handling.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur 45443a2c3a app: [Windows] don't recompute window size during fullscreen switch
The redraw machinery takes care of it automatically.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur 5bdf5950b2 app: [Windows] support app.Decorated(false)
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur 2973c7fa5a app: [Windows] consolidate window mode switch branches
This change is a refactor to make undecorated window support easier
to implement.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur 5e7bf1716e app: [Windows] don't re-compute window size when maximizing
The redraw machinery will take care of it like any other resize.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur 8ef0ad43cb app: [Windows] share extended window styles across Win32 calls
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur df43ba8be0 app: [Windows] don't add or remove the WS_MAXIMIZE flag
ShowWindow(SW_SHOWMAXIMIZED/SW_SHOWNORMAL) is what changes the
window size and state.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur 5cf657065e app: [Windows] use SWP_NOZORDER instead of SWP_NOOWNERZORDER
As I read the SetWindowPos documentation, SWP_NOZORDER is the correct
flag for keeping the z-order of the window.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur 59480066b4 app: [Windows] don't assume fullscreen when not WS_OVERLAPPEDWINDOW
We're about to remove the WS_OVERLAPPEDWINDOW style for undecorated
windows, in which case the fullscreen assumption will no longer hold.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:32 +02:00
Elias Naur 69e4a3cff3 app: guarantee a ConfigEvent for every Window.Configure call
Not only is the client guaranteed a ConfigEvent, but app.Window
can assume that an unsupported decoration change will be corrected
(by a ConfigEvent with Decorated forced to the supported value).

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-25 18:41:22 +02:00
Elias Naur c5e07ba01f app: [macOS] add support for ActionMove
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-24 21:41:56 +02:00
Elias Naur 8457df2d1f app: [macOS] add support for undecorated windows
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-24 20:56:16 +02:00
Elias Naur 8a9382940a widget/material: make DecorationsStyle method receivers by-value
Style values are ephemeral, and pointer methods can't be called in
the same expression a style value is constructed. Matches other style
types.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-24 19:56:56 +02:00
Elias Naur 6a5d3f996a app: don't draw fallback decorations for undecorated windows.
Until now, fallback decorations were only needed for Wayland client-side
decorations. We're about to support app.Decorated(false) one some platforms,
where Window should not fall back to drawing its own decorations.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-23 21:10:57 +02:00
Elias Naur 9f91fecdb3 app: [Wayland] don't allow changes to decoration mode
We're about to enable platform support for switching native
window decorations on and off. However, the Wayland platform
only supports server-side switching of decoration mode, not
(yet) client-side. Thus, don't switch mode even when asked to.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-23 20:33:20 +02:00
Elias Naur 3ca4c98596 gpu/headless: tweak test to pass on MacBook Pro M1
Apparently, there is a rounding error somewhere in the pipeline
from clearing a FBO to downloading its contents on at least one Apple
M1 machine. Tweak the test colors a bit to make it pass.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-23 19:14:53 +02:00
Elias Naur 371de3462b app: replace driver.Close with Perform(ActionClose)
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-23 19:04:30 +02:00
Elias Naur 43116400d0 app: fix racing app.Window.Perform and app.Window.Option
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-23 17:58:45 +02:00
Elias Naur e31aa35622 app: draw fallback decorations on top
Before this change, client-side decorations for Wayland were drawn
before client content, which was prevented from drawing over
decorations with a clip. While visually correct, resize handles would't
work as long as client listeners are near the window edges to swallow
pointer input.

This change makes app.Window draw decorations last, fixing resizing
and saves a clipping operation. This is an alternative to the original
fix for #361, commit 20d4bc2.

References: https://todo.sr.ht/~eliasnaur/gio/361
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-23 13:41:34 +02:00
Chris Waldon 6a6ddc3f19 app: [Wayland] scale min/max window size correctly
The xdg_toplevel expects the min/max window size in DP rather
than pixels. The scaling factor would be applied twice because
we supplied pixels that we scaled ourselves, resulting in windows
twice the expected size on HiDPI screens. This bug probably went
for so long without being detected because it only manifests if
you actually set a minimum or maximum size.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-06-23 10:10:22 +02:00
Chris Waldon 55c96adb91 app: [Wayland] handle multiple global registry event orders
Not all wayland compositors advertise the global registry events
in the same order. In particular, river and sway differ in that
sway advertises the data_device_manager before the seat, and river
does it after. This commit updates our code to correctly bind
the data_device so that we can work with the clipboard regardless
of the registry event order.

Special thanks to Isaac Freund (river maintainer) for helping me
find the root of this problem. You can see Isaac's extremely helpful
and detailed analysis here:

https://github.com/riverwm/river/issues/554#issuecomment-1059750874

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-06-23 10:10:11 +02:00
Egon Elbre 72669e19bc unit: add Metric.DpToSp and Metric.SpToDp
It's sometimes necessary to specify padding or spacing based on
the text size.

Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-06-15 11:30:12 +02:00
Elias Naur bf6371c8e9 app: restore IME snippet after an EditorReplace
Commit 0273203743 removed the snippet
restore event, which broke IME on macOS and Windows.

Fixes: https://todo.sr.ht/~eliasnaur/gio/424
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-15 11:30:12 +02:00
Elias Naur 5ff316ed6d gpu: re-align coverUniforms struct
Direct3D requires GPU vertex attribute structs sizes be a multiple
16. A cleanup commit removed an unusued field, and broke that
assumption.

Fixes: https://todo.sr.ht/~eliasnaur/gio/422
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-15 11:30:05 +02:00
Elias Naur 916efb4612 all: apply suggestions from staticcheck.io
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-07 12:28:28 +02:00
Chris Waldon 11192a5142 text: eliminate path cache memory leak
This commit alters the method we use to check for valid cache hits
in the text path cache. Previously we stored the entire text.Layout
that was provided when the cache entry was set so that we could ensure
only identical text.Layouts would produce hits (guarding against hash
collisions). This commit instead pulls the glyph IDs for every glyph
in the text.Layout and stores them in the cache. This uses far less
memory and seems to allow cache entries to be GCed after eviction.

Fixes: https://todo.sr.ht/~eliasnaur/gio/418
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-06-07 12:28:28 +02:00
Elias Naur 9bf68963da flake.lock: run nix flake update
Otherwise, running Gio programs fails with an error because of
GPU driver mismatch.

References: https://todo.sr.ht/~eliasnaur/gio/417
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-07 12:28:28 +02:00
Elias Naur a10a8a816a .gitignore: delete
There was nothing relevant in it.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-07 12:28:28 +02:00
Elias Naur 4872bd3cd6 Revert "flake.lock: [NixOS] delete to avoid GPU driver version mismatch"
This reverts commit a896a467ec. Because
of

	https://github.com/NixOS/nix/issues/5810

not having flake.lock checked in makes the `nix` command add it instead,
regardless of its presence in `.gitignore`.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-07 12:28:12 +02:00
Chris Waldon 44ec48d325 io/router: fix focused key event propagation
When a key.InputOp is focused, keypresses that it does not explicitly
include in its key set should check for ancestor clip areas that are
interested in them. Previously this check only included ancestors of
the final clip area in the hit tree, and could fail to find ancestors
of the focused key.InputOp because they were in a different branch.

This commit also adds a test to lock in the new behavior.

This can likely be made more efficient by adding a rapid way to map
from the focused key tag to its index in the hit tree. I wasn't sure
whether the complexity was warranted, but I'm happy to do that if
it's desired.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-06-01 19:52:59 +02:00
Elias Naur a896a467ec flake.lock: [NixOS] delete to avoid GPU driver version mismatch
A `flake.lock` file pins the versions of packages Gio requires to build.
However, pinning the `libGL` package may result in a runtime incompatibility
between the user space GPU driver and the system GPU driver. The result is
an error such as

	glGetDisplay failed: 0x300

I don't know a way to keep the convenience of pinned flake.lock versions
combined with a system-defined `libGL` package version. This change implements
a workaround: exclude `flake.lock` from version control and let the first
use of `flake.nix` auto-create a lock file that (hopefully) is compatible
with the system.

References: https://todo.sr.ht/~eliasnaur/gio/417
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-06-01 12:01:44 +02:00
Elias Naur b5f12c5f26 f32: [API] unexport Rectangle
There are no public API that uses f32.Rectangle anymore. Move Rectangle
to an internal package for internal use.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-31 10:24:09 +02:00
Elias Naur fc79ec5c94 layout: [API] remove FRect
We're about to unexport f32.Rectangle, this change removes the only
public API for it.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-31 10:24:09 +02:00
Elias Naur 3d37491342 all: [API] replace unit.Value with separate unit.Dp, unit.Sp types
The unit.Value is a struct and thus more inconvenient to use than its
underlying float32 type. In addition, most uses don't need a general
value, but rather a specific unit given by the context. This change
replaces unit.Value with two float32 units, Dp and Sp. It also changes
variables and parameters of unit.Value to a specific unit type matching
the context. That is, unit.Dp everywhere except for text sizes which are
in Sp.

Switching to typed float32s has multiple advantages

- They can be constants:

const touchSlop = unit.Dp(16)

- Casting untyped constants is no longer necessary:

insets := layout.UniformInset(16)

- Calculation with values is natural:

func (s ScrollbarStyle) Width() unit.Dp {
	return s.Indicator.MinorWidth + s.Track.MinorPadding + s.Track.MinorPadding
}

The main API change is that calls to gtx.Px must be replaced with either
gtx.Dp or gtx.Sp depending on the unit.

Idea by Christophe Meessen.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-31 10:24:09 +02:00
Elias Naur 48a8540a68 all: [API] change clip.RRect and UniformRRect to take integer coordinates
Like the change to op.Offset before this, clip.RRect and UniformRRect
is usually used with integer coordinates. Change to integer coordinates
to eliminate many useless conversions to float32.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-31 10:24:09 +02:00
Elias Naur a63e0cb44a all: [API] change op.Offset to take integer coordinates
op.Offset is a convenience function most often used by layouts. Layouts
usually operate in integer coordinates, and the float32 version of op.Offset
needlessly force conversions from int to float32. This change makes op.Offset
take integer coordinates, to better match its intended use.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-31 10:24:09 +02:00
Elias Naur 14805af367 gesture,widget,f32: [API] use integer coordinates for gesture coordinates
Most widget code operate in integer coordinates. This change makes
gesture pointer coordinates integer, to lessen the number of float32
to int conversions.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-31 10:24:09 +02:00
Chris Waldon 87be31cbec widget/material: ensure scrollbar within dimensions
This commit fixes a visual-only bug in the ListStyle that could
make the scrollbar float at the edge of the maximum constraints
when the list did not occupy the full constraints. The list
would still reserve layout space for the scrollbar in the correct
position, but the scrollbar would not be displayed there.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-05-31 09:37:27 +02:00
Chris Waldon 99d0332067 widget/material: prevent invalid list item constraints
Previously, a bug in the ListStyle could result in items being
passed a negative value in the minimum constraints.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-05-31 09:37:09 +02:00
Egon Elbre cbbb5865e5 app/internal/windows: fix WS_CLIPCHILDREN value
Fixes: https://todo.sr.ht/~eliasnaur/gio/419
Signed-off-by: Egon Elbre <egonelbre@gmail.com>
2022-05-31 09:25:12 +02:00
Elias Naur 2a0a196d1a app: don't deadlock if Window.validateAndProcess fails
Fixes: https://todo.sr.ht/~eliasnaur/gio/417
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-27 11:31:13 +02:00
Chris Waldon 28acb79b82 text: fix doc typos
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-05-27 11:17:39 +02:00
Elias Naur 79f037f983 app: lock GPU context during present
The OpenGL backend needs it, but I keep forgetting to test it when
rearranging the window rendering code. The gogio X11 end-to-end test
tests this issue, but unfortunately it is disabled because of flakiness.

Fixes: https://todo.sr.ht/~eliasnaur/gio/412
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-17 09:30:35 +02:00
Elias Naur 0e2e02a662 app: don't lock up when using custom renderers
A recent change broke custom rendering by not allowing the client
to continue after calling FrameEvent.Frame. This change makes sure
the client is allowed to continue regardless of rendering mode.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-10 21:41:37 +02:00
Elias Naur 7fc594fa4b io/key: remove key.NameUp/Down/Left/Right
They're no longer used now that Android directional keys are mapped
to key.Name*Arrow.

References: https://todo.sr.ht/~eliasnaur/gio/410
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-10 16:49:59 +02:00
Mearaj 7ced0d29ab app,widget: use arrow keys for Android navigation
Android doesn't distinguish between the arrow keys on a keyboard and the
directional keys on a remote control, so there's no way to move the caret
in an Editor with arrow keys. This change updates the Android port to map
Android's DPAD_* key codes to the arrow key names, fixing caret movement.
The change also updates Editor to only request arrow keys that actually move
the caret, to keep directional focus movement working.

Fixes: https://todo.sr.ht/~eliasnaur/gio/410
Signed-off-by: Mearaj <mearajbhagad@gmail.com>
2022-05-10 16:41:32 +02:00
Elias Naur c68417aaf9 go.*: upgrade github.com/benoitkugler/textlayout to v0.1.1
The v0.1.1 release is much smaller because the module no longer contains
test data. See

https://github.com/benoitkugler/textlayout/issues/10

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-05-07 09:31:44 +02:00
Chris Waldon 4996337d26 widget: ensure empty editor makes space for caret
Prior to this change an editor with no content and a zero minimum
constraint would return itself has having width zero. This
prevented users from being able to see the editor when they
moved focus to it, as it could not display its caret. This
simple change ensures that, at minimum, the editor returns
its dimensions to include the width of a caret.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-05-07 09:14:10 +02:00
Inkeliz c97f976e7d app: [Android] improve keyboard hints
This patch adds support for the following KeyboardHint: Text, Email,
Telephone and URL.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
2022-05-06 09:18:06 +02:00
Thomas Mathews 45e8c781e2 layout: improve layout.List documentation
Updated the documentation for layout.List to include the details about how
drawing is performed for items in it. This gives the user an understanding about
how so many items can be drawn for performance.

Signed-off-by: Thomas Mathews <thomas.c.mathews@gmail.com>
2022-05-03 07:57:28 +02:00
Elias Naur a9a9a7c02f go.*: upgrade github.com/benoitkugler/textlayout to v0.1.0
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-28 11:45:41 +02:00
Elias Naur aa14056350 io/router: remove unused frect function
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-25 09:12:42 +02:00
Elias Naur 4e6a9c0909 cmd: delete tools module
It has moved to its own repository:

https://git.sr.ht/~eliasnaur/gio-cmd

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-24 14:39:16 +02:00
Elias Naur 1071f56119 app: only perform actions and apply options on wakeups
In particular, avoid a race between the setup of the platform window
returned by NewWindow and Window.Perform.

Fixes: https://todo.sr.ht/~eliasnaur/gio/405
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-24 12:51:53 +02:00
Elias Naur d22ec125ea app: replace Config.center with Perform(ActionCenter)
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-24 11:51:20 +02:00
Elias Naur 1a833ab0a4 app: replace driver.Raise with Perform(ActionRaise)
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-24 11:36:06 +02:00
Elias Naur 6ede60d84e app: [Android] avoid out-of-bounds access in getCursorCapsMode
Fixes: https://todo.sr.ht/~eliasnaur/gio/404
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-24 10:50:48 +02:00
Elias Naur 8630fee623 io/pointer: remove unused functions
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-23 16:18:32 +02:00
Elias Naur 3c45a6d420 widget: don't draw Editor selection when not focused
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-23 15:45:41 +02:00
Elias Naur 2381c5ad70 io/router,widget: give every key.InputOp a chance to process events
If the currently focused handler don't want the key event, try every
other handler, from top to bottom. This change requires widgets to
only react when focused.

Fixes: https://todo.sr.ht/~eliasnaur/gio/406
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-23 15:36:45 +02:00
Elias Naur 0273203743 app,io/router: expand IME snippets if a new range overlaps the old
Instead of cmpletely replacing the IME snippet for every update, expand
the old range if there is overlap. This change avoids never-ending
restarts of the IME on Android where snippets are expanded in two
calls, one for expanding before the selection and one for exanding after
the selection.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-23 13:20:46 +02:00
Elias Naur f3265e56b9 app: [Android] take snippet offset into account for getCursorCapsMode
References: https://todo.sr.ht/~eliasnaur/gio/404
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-23 11:40:43 +02:00
Elias Naur 30fa85f518 io/router: send key events to root handlers if nothing else wants them
Fixes: https://todo.sr.ht/~eliasnaur/gio/403
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-23 09:40:23 +02:00
Elias Naur 6ddc13ce66 widget: fix Editor key set
Arrow and delete/backspace shortcuts use ShortcutAlt, not Shortcut.

References: https://todo.sr.ht/~eliasnaur/gio/399
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-19 08:58:47 +02:00
Elias Naur 7629874237 widget/material: remove redundant offset op
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-19 08:47:15 +02:00
Elias Naur 3a4b8b81ec io/system: describe FrameEvent.Insets more precisely
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-19 08:47:15 +02:00
Elias Naur 6c76fa6dec app,io/key,io/system: [API] replace system.CommandEvent with key.Event
It's much simpler to map the Android back button to a key.Event and
let the usual key filtering determine whether to block its default
behaviour.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-19 08:47:15 +02:00
Elias Naur ad7c1eb78d app,io/key: introduce keys for directional navigation
This change adds key.NameUp/Down/Left/Right and maps the Android TV
remote directional keys to them. As a side-effect a key.InputOp can
now receive directional keys (and block their focus movement).

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-14 19:09:08 +02:00
Elias Naur d37197f45b app: give key handlers a chance to process Tab and Shift-Tab
Before this change, Tab and Shift-Tab would always result in focus
movement. This this change, a key.InputOp with a matching Keys set
will block focus movement and deliver the events to it.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-14 19:09:08 +02:00
Elias Naur 380f96b3fc io/key: [API] implement key event propagation
Before this change, every Event would be passed to the focused InputOp
tag, making it impossible to implement, say, program-wide shortcuts.
This change implements key.Event routing similar to how pointer.Events
are routed: every InputOp describes the set of keys it can handle, and
the router use that information to deliver an Event to the matching
handler.

This is an API change, because every InputOp must now include a filter
matching the keys it wants to handle.

Fixes: https://todo.sr.ht/~eliasnaur/gio/395
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-14 19:09:00 +02:00
Elias Naur bec0283e54 io/router: deliver synthetic events to sibling pointer handlers
Before this change, synthetic events such as scrolling caused by
focus movement would use semantic information to determine potential
receivers. However, there can only be one handler per area so sibling
handlers would not be considered. This change makes the event delivery
traverse the entire tree of handlers, including siblings.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-14 18:51:39 +02:00
Elias Naur 9c59612e08 io/key: change Modifiers.String separator to "-"
We're about the express sets of key combinations as <modifiers>-<keys>
where modifiers are separated by dashes as well. To make Modifers.String
useful for expressing key sets, change its separator to "-".

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-14 11:48:55 +02:00
Elias Naur ed8d3e8566 io/key: [API] rename tab and modifier keys, introduce NameCommand
We already have precedence for word-named keys ("Space") and the new
names are less obscure and matches Modifiers.String.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-14 11:45:08 +02:00
Elias Naur dc25afda07 app: don't panic when the client doesn't call FrameEvent.Frame
Fixes: https://todo.sr.ht/~eliasnaur/gio/396
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-14 09:16:49 +02:00
Elias Naur 6a14269682 gpu/internal/opengl: add fallback for sparse OpenGL ES 2.0 ReadPixels
OpenGL ES 2.0 doesn't support GL_PACK_ROW_LENGTH, so this change implements
a fallback using a temporary buffer.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-13 17:28:36 +02:00
Elias Naur 43865ddabd app: don't delay FrameEvent.Frame by v-sync latency
We should return as soon as possible from FrameEvent.Frame to allow the main
goroutine to continue processing other tasks.  Whereas GPU.Frame may touch
the frame ops, GPU.Present will not, so this change moves Present to after
returning from FrameEvent.Frame.

This is a variant of 38ff78df5d that
works on OpenGL where context locking is required.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-13 16:05:28 +02:00
Elias Naur 405215f862 Revert "app: don't delay FrameEvent.Frame by v-sync latency"
This reverts commit 38ff78df5d, because
it broke OpenGL by moving eglSwapBuffers outside the MakeCurrent
context scope.

Fixes: https://todo.sr.ht/~eliasnaur/gio/393
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-13 16:01:25 +02:00
Elias Naur 6e66203881 gpu/headless: return error if NewTexture fails
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-13 12:58:33 +02:00
Elias Naur 38ff78df5d app: don't delay FrameEvent.Frame by v-sync latency
We should return as soon as possible from FrameEvent.Frame to
allow the main goroutine to continue processing other tasks.
Whereas GPU.Frame may touch the frame ops, GPU.Present will not,
so this change moves Present to after returning from FrameEvent.Frame.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-12 18:21:23 +02:00
Chris Waldon 25fae8de30 deps: update golang.org/x/text and go-text
Fixes a panic parsing language tags.

References: https://go-review.googlesource.com/c/text/+/340830
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2022-04-12 09:11:42 +02:00
Elias Naur 49bd5787e4 app: [macOS] fix caret position calculation after IME text insert
Fixes: https://todo.sr.ht/~eliasnaur/gio/385
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2022-04-08 13:05:27 +02:00
Jack Mordaunt 4e488f4c70 app: [API] don't relay raw input events from app.Window
Avoid sending raw inputs events over the window channel.

If the caller wants to access events, they should be
using input handlers and querying for events.

This change avoids saturating the channel with less important
messages which can affect frame event latency, especially in
the case of custom rendering.

See also

https://lists.sr.ht/~eliasnaur/gio/%3CCAFcc3FQNTp_UXr7oA97SsVPD7D91jSw30ZtALcT9vmopFDTeZQ%40mail.gmail.com%3E

Signed-off-by: Jack Mordaunt <jackmordaunt.dev@gmail.com>
2022-04-07 13:18:59 +02:00
285 changed files with 23514 additions and 21477 deletions
+19 -19
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Unlicense OR MIT # SPDX-License-Identifier: Unlicense OR MIT
image: debian/testing image: debian/stable
packages: packages:
- clang - clang
- cmake - cmake
@@ -8,23 +8,28 @@ packages:
- libxml2-dev - libxml2-dev
- libssl-dev - libssl-dev
- libz-dev - libz-dev
- llvm-dev # for cctools - llvm-dev # cctools
- uuid-dev ## for cctools - uuid-dev # cctools
- ninja-build # cctools
- systemtap-sdt-dev # cctools
- libbsd-dev # cctools
- linux-libc-dev # cctools
- libplist-utils # for gogio - libplist-utils # for gogio
sources: sources:
- https://git.sr.ht/~eliasnaur/applesdks - https://git.sr.ht/~eliasnaur/applesdks
- https://git.sr.ht/~eliasnaur/gio - https://git.sr.ht/~eliasnaur/gio
- https://git.sr.ht/~eliasnaur/giouiorg - https://git.sr.ht/~eliasnaur/giouiorg
- https://github.com/tpoechtrager/cctools-port.git - https://github.com/tpoechtrager/cctools-port
- https://github.com/tpoechtrager/apple-libtapi.git - https://github.com/tpoechtrager/apple-libtapi
- https://github.com/mackyle/xar.git - https://github.com/tpoechtrager/apple-libdispatch
- https://github.com/mackyle/xar
environment: environment:
APPLE_TOOLCHAIN_ROOT: /home/build/appletools APPLE_TOOLCHAIN_ROOT: /home/build/appletools
PATH: /home/build/sdk/go/bin:/home/build/go/bin:/usr/bin PATH: /home/build/sdk/go/bin:/home/build/go/bin:/usr/bin
tasks: tasks:
- install_go: | - install_go: |
mkdir -p /home/build/sdk mkdir -p /home/build/sdk
curl -s https://dl.google.com/go/go1.17.7.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: | - prepare_toolchain: |
mkdir -p $APPLE_TOOLCHAIN_ROOT mkdir -p $APPLE_TOOLCHAIN_ROOT
cd $APPLE_TOOLCHAIN_ROOT cd $APPLE_TOOLCHAIN_ROOT
@@ -42,6 +47,11 @@ tasks:
- install_appletoolchain: | - install_appletoolchain: |
cd giouiorg cd giouiorg
go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain 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: | - build_xar: |
cd xar/xar cd xar/xar
ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr
@@ -53,7 +63,7 @@ tasks:
./install.sh ./install.sh
- build_cctools: | - build_cctools: |
cd cctools-port/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 make install
- test_macos: | - test_macos: |
cd gio cd gio
@@ -61,14 +71,4 @@ tasks:
CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./... CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./...
- test_ios: | - test_ios: |
cd gio 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 ./...
- install_gogio: |
cd gio/cmd
go install ./gogio
- test_ios_gogio: |
mkdir tmp
cd tmp
go mod init example.com
go get -d gioui.org/example/kitchen
export PATH=/home/build/appletools/bin:$PATH
gogio -target ios -o app.app gioui.org/example/kitchen
+2 -5
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Unlicense OR MIT # SPDX-License-Identifier: Unlicense OR MIT
image: freebsd/13.x image: freebsd/latest
packages: packages:
- libX11 - libX11
- libxkbcommon - libxkbcommon
@@ -16,10 +16,7 @@ environment:
tasks: tasks:
- install_go: | - install_go: |
mkdir -p /home/build/sdk mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.17.7.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: | - test_gio: |
cd gio cd gio
go test ./... go test ./...
- test_cmd: |
cd gio/cmd
go test ./...
+20 -25
View File
@@ -1,8 +1,9 @@
# SPDX-License-Identifier: Unlicense OR MIT # SPDX-License-Identifier: Unlicense OR MIT
image: debian/testing image: debian/stable
packages: packages:
- curl - curl
- pkg-config - pkg-config
- gcc-multilib
- libwayland-dev - libwayland-dev
- libx11-dev - libx11-dev
- libx11-xcb-dev - libx11-xcb-dev
@@ -17,6 +18,12 @@ packages:
- libxinerama-dev - libxinerama-dev
- libxi-dev - libxi-dev
- libxxf86vm-dev - libxxf86vm-dev
- libegl-mesa0
- libglx-mesa0
- libgl1-mesa-dri
- mesa-libgallium
- libgbm1
- libegl1
- mesa-vulkan-drivers - mesa-vulkan-drivers
- wine - wine
- xvfb - xvfb
@@ -24,12 +31,11 @@ packages:
- scrot - scrot
- sway - sway
- grim - grim
- wine
- unzip - unzip
sources: sources:
- https://git.sr.ht/~eliasnaur/gio - https://git.sr.ht/~eliasnaur/gio
environment: environment:
GOFLAGS: -mod=readonly PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig/:/usr/lib/i386-linux-gnu/pkgconfig/
PATH: /home/build/sdk/go/bin:/usr/bin:/home/build/go/bin:/home/build/android/tools/bin PATH: /home/build/sdk/go/bin:/usr/bin:/home/build/go/bin:/home/build/android/tools/bin
ANDROID_SDK_ROOT: /home/build/android ANDROID_SDK_ROOT: /home/build/android
android_sdk_tools_zip: sdk-tools-linux-3859397.zip android_sdk_tools_zip: sdk-tools-linux-3859397.zip
@@ -40,7 +46,7 @@ secrets:
tasks: tasks:
- install_go: | - install_go: |
mkdir -p /home/build/sdk mkdir -p /home/build/sdk
curl -s https://dl.google.com/go/go1.17.7.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: | - check_gofmt: |
cd gio cd gio
test -z "$(gofmt -s -l .)" test -z "$(gofmt -s -l .)"
@@ -54,20 +60,19 @@ tasks:
exit 1 exit 1
fi fi
done done
- mirror: |
# mirror to github
ssh-keyscan github.com > "$HOME"/.ssh/known_hosts && cd gio && git push --mirror "$github_mirror" || echo "failed mirroring"
- add_32bit_arch: |
sudo dpkg --add-architecture i386
sudo apt-get update
sudo apt-get install -y "libwayland-dev:i386" "libx11-dev:i386" "libx11-xcb-dev:i386" "libxkbcommon-dev:i386" "libxkbcommon-x11-dev:i386" "libgles2-mesa-dev:i386" "libegl1-mesa-dev:i386" "libffi-dev:i386" "libvulkan-dev:i386" "libxcursor-dev:i386" "libegl-mesa0:i386" "libglx-mesa0:i386" "libgbm1:i386" "mesa-libgallium:i386" "libgl1-mesa-dri:i386"
- test_gio: | - test_gio: |
cd gio cd gio
go test -race ./... go test -race ./...
CGO_ENABLED=1 GOARCH=386 go test ./...
GOOS=windows go test -exec=wine ./... GOOS=windows go test -exec=wine ./...
GOOS=js GOARCH=wasm go build -o /dev/null ./... 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
- test_cmd: |
cd gio/cmd
go test ./...
go test -race ./...
- install_jdk8: | - install_jdk8: |
curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb" 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 sudo apt-get -qq install -y -f ./jdk.deb
@@ -81,21 +86,11 @@ tasks:
unzip -q ndk.zip unzip -q ndk.zip
rm ndk.zip rm ndk.zip
mv android-ndk-* ndk-bundle mv android-ndk-* ndk-bundle
# sdkmanager needs lots of file descriptors
ulimit -n 10000
yes|sdkmanager --licenses yes|sdkmanager --licenses
sdkmanager "platforms;android-31" "build-tools;32.0.0" sdkmanager "platforms;android-31" "build-tools;32.0.0"
- test_android: | - test_android: |
cd gio cd gio
CC=$ANDROID_SDK_ROOT/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./... CC=$ANDROID_SDK_ROOT/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android29-clang GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build ./...
CC=$ANDROID_SDK_ROOT/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang GOOS=android GOARCH=arm CGO_ENABLED=1 go build ./... CC=$ANDROID_SDK_ROOT/ndk-bundle/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi29-clang GOOS=android GOARCH=arm CGO_ENABLED=1 go build ./...
- install_gogio: |
cd gio/cmd
go install ./gogio
- test_android_gogio: |
mkdir tmp
cd tmp
go mod init example.com
go get -d gioui.org/example/kitchen
gogio -target android gioui.org/example/kitchen
- mirror: |
# mirror to github
ssh-keyscan github.com > "$HOME"/.ssh/known_hosts && cd gio && git push --mirror "$github_mirror" || echo "failed mirroring"
+1 -4
View File
@@ -10,12 +10,9 @@ environment:
tasks: tasks:
- install_go: | - install_go: |
mkdir -p /home/build/sdk mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.17.7.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 cd /home/build/sdk/go/src
./make.bash ./make.bash
- test_gio: | - test_gio: |
cd gio cd gio
go test ./... go test ./...
- test_cmd: |
cd gio/cmd
go test ./...
-2
View File
@@ -1,2 +0,0 @@
.gradle
**/android/build
+9
View File
@@ -24,3 +24,12 @@ account is required and you can post without being subscribed.
See the [contribution guide](https://gioui.org/doc/contribute) for more details. See the [contribution guide](https://gioui.org/doc/contribute) for more details.
An [official GitHub mirror](https://github.com/gioui/gio) is available. An [official GitHub mirror](https://github.com/gioui/gio) is available.
## Tags
Pre-1.0 tags are provided for reference only, and do not designate releases with ongoing support. Bugfixes will not be backported to older tags.
Tags follow semantic versioning. In particular, as the major version is zero:
- breaking API or behavior changes will increment the *minor* version component.
- non-breaking changes will increment the *patch* version component.
+34 -3
View File
@@ -4,17 +4,33 @@ package org.gioui;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.view.ViewGroup;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
public final class GioActivity extends Activity { public final class GioActivity extends Activity {
private GioView view; private GioView view;
public FrameLayout layer;
@Override public void onCreate(Bundle state) { @Override public void onCreate(Bundle state) {
super.onCreate(state); super.onCreate(state);
this.view = new GioView(this); layer = new FrameLayout(this);
view = new GioView(this);
setContentView(view); view.setLayoutParams(new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
));
view.setFocusable(true);
view.setFocusableInTouchMode(true);
layer.addView(view);
setContentView(layer);
onNewIntent(this.getIntent());
} }
@Override public void onDestroy() { @Override public void onDestroy() {
@@ -32,6 +48,16 @@ public final class GioActivity extends Activity {
super.onStop(); super.onStop();
} }
@Override public void onPause() {
super.onPause();
view.pause();
}
@Override public void onResume() {
super.onResume();
view.resume();
}
@Override public void onConfigurationChanged(Configuration c) { @Override public void onConfigurationChanged(Configuration c) {
super.onConfigurationChanged(c); super.onConfigurationChanged(c);
view.configurationChanged(); view.configurationChanged();
@@ -46,4 +72,9 @@ public final class GioActivity extends Activity {
if (!view.backPressed()) if (!view.backPressed())
super.onBackPressed(); super.onBackPressed();
} }
@Override protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
view.onIntentEvent(intent);
}
} }
+89 -9
View File
@@ -12,6 +12,7 @@ import android.app.Fragment;
import android.app.FragmentManager; import android.app.FragmentManager;
import android.app.FragmentTransaction; import android.app.FragmentTransaction;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Matrix; import android.graphics.Matrix;
@@ -26,6 +27,7 @@ import android.text.SpannableStringBuilder;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.Choreographer; import android.view.Choreographer;
import android.view.Display;
import android.view.KeyCharacterMap; import android.view.KeyCharacterMap;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -60,12 +62,11 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
private static boolean jniLoaded; private static boolean jniLoaded;
private final SurfaceHolder.Callback surfCallbacks; private final SurfaceHolder.Callback surfCallbacks;
private final View.OnFocusChangeListener focusCallback;
private final InputMethodManager imm; private final InputMethodManager imm;
private final float scrollXScale; private final float scrollXScale;
private final float scrollYScale; private final float scrollYScale;
private final AccessibilityManager accessManager;
private int keyboardHint; private int keyboardHint;
private AccessibilityManager accessManager;
private long nhandle; private long nhandle;
@@ -105,17 +106,13 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
scrollYScale = px; scrollYScale = px;
} }
setHighRefreshRate();
accessManager = (AccessibilityManager)context.getSystemService(Context.ACCESSIBILITY_SERVICE); accessManager = (AccessibilityManager)context.getSystemService(Context.ACCESSIBILITY_SERVICE);
imm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); imm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
nhandle = onCreateView(this); nhandle = onCreateView(this);
setFocusable(true); setFocusable(true);
setFocusableInTouchMode(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() { surfCallbacks = new SurfaceHolder.Callback() {
@Override public void surfaceCreated(SurfaceHolder holder) { @Override public void surfaceCreated(SurfaceHolder holder) {
// Ignore; surfaceChanged is guaranteed to be called immediately after this. // Ignore; surfaceChanged is guaranteed to be called immediately after this.
@@ -255,6 +252,72 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
this.setBarColor(Bar.NAVIGATION, color, luminance); this.setBarColor(Bar.NAVIGATION, color, luminance);
} }
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();
if (supportedModes.length <= 1) {
// Nothing to set
return;
}
Display.Mode currentMode = display.getMode();
int currentWidth = currentMode.getPhysicalWidth();
int currentHeight = currentMode.getPhysicalHeight();
float minRefreshRate = -1;
float maxRefreshRate = -1;
float bestRefreshRate = -1;
int bestModeId = -1;
for (Display.Mode mode : supportedModes) {
float refreshRate = mode.getRefreshRate();
float width = mode.getPhysicalWidth();
float height = mode.getPhysicalHeight();
if (minRefreshRate == -1 || refreshRate < minRefreshRate) {
minRefreshRate = refreshRate;
}
if (maxRefreshRate == -1 || refreshRate > maxRefreshRate) {
maxRefreshRate = refreshRate;
}
boolean refreshRateIsBetter = bestRefreshRate == -1 || refreshRate > bestRefreshRate;
if (width == currentWidth && height == currentHeight && refreshRateIsBetter) {
int modeId = mode.getModeId();
bestRefreshRate = refreshRate;
bestModeId = modeId;
}
}
if (bestModeId == -1) {
// Not expecting this but just in case
return;
}
if (minRefreshRate == maxRefreshRate) {
// Can't improve the refresh rate
return;
}
Window window = ((Activity) context).getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.preferredDisplayModeId = bestModeId;
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) { @Override protected boolean dispatchHoverEvent(MotionEvent event) {
if (!accessManager.isTouchExplorationEnabled()) { if (!accessManager.isTouchExplorationEnabled()) {
return super.dispatchHoverEvent(event); return super.dispatchHoverEvent(event);
@@ -412,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() { public void destroy() {
if (nhandle != 0) { if (nhandle != 0) {
onDestroyView(nhandle); onDestroyView(nhandle);
@@ -493,6 +568,7 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
static private native void onExitTouchExploration(long handle); static private native void onExitTouchExploration(long handle);
static private native void onA11yFocus(long handle, int viewId); static private native void onA11yFocus(long handle, int viewId);
static private native void onClearA11yFocus(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 void imeSetSnippet(long handle, int start, int end);
static private native String imeSnippet(long handle); static private native String imeSnippet(long handle);
static private native int imeSnippetStart(long handle); static private native int imeSnippetStart(long handle);
@@ -555,7 +631,11 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
@Override public int getCursorCapsMode(int reqModes) { @Override public int getCursorCapsMode(int reqModes) {
Snippet snip = getSnippet(); Snippet snip = getSnippet();
int selStart = imeSelectionStart(nhandle); int selStart = imeSelectionStart(nhandle);
return TextUtils.getCapsMode(snip.snippet, imeToUTF16(nhandle, selStart), reqModes); int off = imeToUTF16(nhandle, selStart - snip.offset);
if (off < 0 || off > snip.snippet.length()) {
return 0;
}
return TextUtils.getCapsMode(snip.snippet, off, reqModes);
} }
@Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { @Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+159 -6
View File
@@ -3,8 +3,19 @@
package app package app
import ( import (
"gioui.org/io/event"
"golang.org/x/net/idna"
"image"
"net/url"
"os" "os"
"path/filepath"
"strings" "strings"
"time"
"gioui.org/io/input"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
) )
// extraArgs contains extra arguments to append to // extraArgs contains extra arguments to append to
@@ -14,10 +25,102 @@ import (
// Set with the go linker flag -X. // Set with the go linker flag -X.
var extraArgs string var extraArgs string
func init() { // ID is the app id exposed to the platform.
if extraArgs != "" { //
args := strings.Split(extraArgs, "|") // On Android ID is the package property of AndroidManifest.xml,
os.Args = append(os.Args, args...) // on iOS ID is the CFBundleIdentifier of the app Info.plist,
// on Wayland it is the toplevel app_id,
// on X11 it is the X11 XClassHint.
//
// ID is set by the [gioui.org/cmd/gogio] tool or manually with the -X linker flag. For example,
//
// go build -ldflags="-X 'gioui.org/app.ID=org.gioui.example.Kitchen'" .
//
// Note that ID is treated as a constant, and that changing it at runtime
// is not supported. The default value of ID is filepath.Base(os.Args[0]).
var ID = ""
// A FrameEvent requests a new frame in the form of a list of
// operations that describes the window content.
type FrameEvent struct {
// Now is the current animation. Use Now instead of time.Now to
// synchronize animation and to avoid the time.Now call overhead.
Now time.Time
// Metric converts device independent dp and sp to device pixels.
Metric unit.Metric
// Size is the dimensions of the window.
Size image.Point
// Insets represent the space occupied by system decorations and controls.
Insets Insets
// Frame completes the FrameEvent by drawing the graphical operations
// from ops into the window.
Frame func(frame *op.Ops)
// Source is the interface between the window and widgets.
Source input.Source
}
// 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)
}
return layout.Context{
Ops: ops,
Now: e.Now,
Source: e.Source,
Metric: e.Metric,
Constraints: layout.Exact(size),
} }
} }
@@ -27,8 +130,7 @@ func init() {
// On iOS NSDocumentDirectory is queried. // On iOS NSDocumentDirectory is queried.
// For Android Context.getFilesDir is used. // For Android Context.getFilesDir is used.
// //
// BUG: DataDir blocks on Android until init functions // BUG: On Android, DataDir panics if called before main.
// have completed.
func DataDir() (string, error) { func DataDir() (string, error) {
return dataDir() return dataDir()
} }
@@ -44,3 +146,54 @@ func DataDir() (string, error) {
func Main() { func Main() {
osMain() 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
}
+8 -8
View File
@@ -20,7 +20,7 @@ type d3d11Context struct {
width, height int width, height int
} }
const debug = false const debugDirectX = false
func init() { func init() {
drivers = append(drivers, gpuAPI{ drivers = append(drivers, gpuAPI{
@@ -28,7 +28,7 @@ func init() {
initializer: func(w *window) (context, error) { initializer: func(w *window) (context, error) {
hwnd, _, _ := w.HWND() hwnd, _, _ := w.HWND()
var flags uint32 var flags uint32
if debug { if debugDirectX {
flags |= d3d11.CREATE_DEVICE_DEBUG flags |= d3d11.CREATE_DEVICE_DEBUG
} }
dev, ctx, _, err := d3d11.CreateDevice( dev, ctx, _, err := d3d11.CreateDevice(
@@ -60,10 +60,10 @@ func (c *d3d11Context) RenderTarget() (gpu.RenderTarget, error) {
} }
func (c *d3d11Context) Present() error { func (c *d3d11Context) Present() error {
err := c.swchain.Present(1, 0) return wrapErr(c.swchain.Present(1, 0))
if err == nil { }
return nil
} func wrapErr(err error) error {
if err, ok := err.(d3d11.ErrorCode); ok { if err, ok := err.(d3d11.ErrorCode); ok {
switch err.Code { switch err.Code {
case d3d11.DXGI_STATUS_OCCLUDED: case d3d11.DXGI_STATUS_OCCLUDED:
@@ -84,7 +84,7 @@ func (c *d3d11Context) Refresh() error {
} }
c.releaseFBO() c.releaseFBO()
if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, 0); err != nil { if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, 0); err != nil {
return err return wrapErr(err)
} }
c.width = width c.width = width
c.height = height c.height = height
@@ -122,7 +122,7 @@ func (c *d3d11Context) Release() {
d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release) d3d11.IUnknownRelease(unsafe.Pointer(c.dev), c.dev.Vtbl.Release)
} }
*c = d3d11Context{} *c = d3d11Context{}
if debug { if debugDirectX {
d3d11.ReportLiveObjects() d3d11.ReportLiveObjects()
} }
} }
-1
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build !android //go:build !android
// +build !android
package app package app
+20 -22
View File
@@ -6,23 +6,22 @@ functionality for running graphical user interfaces.
See https://gioui.org for instructions to set up and run Gio programs. See https://gioui.org for instructions to set up and run Gio programs.
Windows # Windows
Create a new Window by calling NewWindow. On mobile platforms or when Gio A Window is run by calling its Event method in a loop. The first time a
is embedded in another project, NewWindow merely connects with a previously method on Window is called, a new GUI window is created and shown. On mobile
created window. 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 The most important event is [FrameEvent] that prompts an update of the window
important event is FrameEvent that prompts an update of the window contents.
contents and state.
For example: For example:
import "gioui.org/unit" w := new(app.Window)
for {
w := app.NewWindow() e := w.Event()
for e := range w.Events() { if e, ok := e.(app.FrameEvent); ok {
if e, ok := e.(system.FrameEvent); ok {
ops.Reset() ops.Reset()
// Add operations to ops. // Add operations to ops.
... ...
@@ -32,9 +31,9 @@ For example:
} }
A program must keep receiving events from the event channel until A program must keep receiving events from the event channel until
DestroyEvent is received. [DestroyEvent] is received.
Main # Main
The Main function must be called from a program's main function, to hand over The Main function must be called from a program's main function, to hand over
control of the main thread to operating systems that need it. control of the main thread to operating systems that need it.
@@ -49,21 +48,20 @@ For example, to display a blank but otherwise functional window:
func main() { func main() {
go func() { go func() {
w := app.NewWindow() w := new(app.Window)
for range w.Events() { for {
w.Event()
} }
}() }()
app.Main() app.Main()
} }
# Events
Event queue The [Events] iterator yields app-specific events such as [URLEvent]. [Window.Event]
yields events that target a particular window.
A FrameEvent's Queue method returns an event.Queue implementation that distributes # Permissions
incoming events to the event handlers declared in the last frame.
See the gioui.org/io/event package for more information about event handlers.
Permissions
The packages under gioui.org/app/permission should be imported The packages under gioui.org/app/permission should be imported
by a Gio program or by one of its dependencies to indicate that specific by a Gio program or by one of its dependencies to indicate that specific
+6 -6
View File
@@ -1,5 +1,7 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build !noopengl
package app package app
/* /*
@@ -15,9 +17,8 @@ import (
) )
type androidContext struct { type androidContext struct {
win *window win *window
eglSurf egl.NativeWindowType eglSurf egl.NativeWindowType
width, height int
*egl.Context *egl.Context
} }
@@ -43,9 +44,8 @@ func (c *androidContext) Refresh() error {
if err := c.win.setVisual(c.Context.VisualID()); err != nil { if err := c.win.setVisual(c.Context.VisualID()); err != nil {
return err return err
} }
win, width, height := c.win.nativeWindow() win, _, _ := c.win.nativeWindow()
c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win)) c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win))
c.width, c.height = width, height
return nil return nil
} }
@@ -53,7 +53,7 @@ func (c *androidContext) Lock() error {
// The Android emulator creates a broken surface if it is not // The Android emulator creates a broken surface if it is not
// created on the same thread as the context is made current. // created on the same thread as the context is made current.
if c.eglSurf != nil { 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 return err
} }
c.eglSurf = nil c.eglSurf = nil
+22 -14
View File
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build ((linux && !android) || freebsd) && !nowayland //go:build ((linux && !android) || freebsd) && !nowayland && !noopengl
// +build linux,!android freebsd // +build linux,!android freebsd
// +build !nowayland // +build !nowayland
// +build !noopengl
package app package app
@@ -37,7 +38,24 @@ func init() {
if err != nil { if err != nil {
return nil, err 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
} }
} }
@@ -53,22 +71,12 @@ func (c *wlContext) Release() {
} }
func (c *wlContext) Refresh() error { 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() surf, width, height := c.win.surface()
if surf == nil { if surf == nil {
return errors.New("wayland: no surface") return errors.New("wayland: no surface")
} }
eglWin := C.wl_egl_window_create(surf, C.int(width), C.int(height)) C.wl_egl_window_resize(c.eglWin, C.int(width), C.int(height), 0, 0)
if eglWin == nil { return 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)
} }
func (c *wlContext) Lock() error { func (c *wlContext) Lock() error {
+14 -17
View File
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build !noopengl
package app package app
import ( import (
"golang.org/x/sys/windows"
"gioui.org/internal/egl" "gioui.org/internal/egl"
) )
@@ -22,6 +22,18 @@ func init() {
if err != nil { if err != nil {
return nil, err 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 return &glContext{win: w, Context: ctx}, nil
}, },
}) })
@@ -35,21 +47,6 @@ func (c *glContext) Release() {
} }
func (c *glContext) Refresh() error { 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 return nil
} }
+14 -12
View File
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build ((linux && !android) || freebsd || openbsd) && !nox11 //go:build ((linux && !android) || freebsd || openbsd) && !nox11 && !noopengl
// +build linux,!android freebsd openbsd // +build linux,!android freebsd openbsd
// +build !nox11 // +build !nox11
// +build !noopengl
package app package app
@@ -24,6 +25,18 @@ func init() {
if err != nil { if err != nil {
return nil, err 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 return &x11Context{win: w, Context: ctx}, nil
} }
} }
@@ -36,17 +49,6 @@ func (c *x11Context) Release() {
} }
func (c *x11Context) Refresh() error { 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 return nil
} }
-1
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build darwin && ios && nometal //go:build darwin && ios && nometal
// +build darwin,ios,nometal
package app package app
+14 -4
View File
@@ -13,6 +13,7 @@ import (
type glContext struct { type glContext struct {
ctx js.Value ctx js.Value
cnv js.Value cnv js.Value
w *window
} }
func newContext(w *window) (*glContext, error) { func newContext(w *window) (*glContext, error) {
@@ -32,11 +33,15 @@ func newContext(w *window) (*glContext, error) {
c := &glContext{ c := &glContext{
ctx: ctx, ctx: ctx,
cnv: w.cnv, cnv: w.cnv,
w: w,
} }
return c, nil return c, nil
} }
func (c *glContext) RenderTarget() (gpu.RenderTarget, error) { func (c *glContext) RenderTarget() (gpu.RenderTarget, error) {
if c.w.contextStatus != contextStatusOkay {
return nil, gpu.ErrDeviceLost
}
return gpu.OpenGLRenderTarget{}, nil return gpu.OpenGLRenderTarget{}, nil
} }
@@ -48,9 +53,6 @@ func (c *glContext) Release() {
} }
func (c *glContext) Present() error { func (c *glContext) Present() error {
if c.ctx.Call("isContextLost").Bool() {
return errors.New("context lost")
}
return nil return nil
} }
@@ -61,7 +63,15 @@ func (c *glContext) Lock() error {
func (c *glContext) Unlock() {} func (c *glContext) Unlock() {}
func (c *glContext) Refresh() error { func (c *glContext) Refresh() error {
return nil switch c.w.contextStatus {
case contextStatusLost:
return errOutOfDate
case contextStatusRestored:
c.w.contextStatus = contextStatusOkay
return gpu.ErrDeviceLost
default:
return nil
}
} }
func (w *window) NewContext() (context, error) { func (w *window) NewContext() (context, error) {
+3 -2
View File
@@ -1,14 +1,12 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build darwin && !ios && nometal //go:build darwin && !ios && nometal
// +build darwin,!ios,nometal
package app package app
import ( import (
"errors" "errors"
"runtime" "runtime"
"unsafe" "unsafe"
"gioui.org/gpu" "gioui.org/gpu"
@@ -16,6 +14,9 @@ import (
) )
/* /*
#cgo CFLAGS: -DGL_SILENCE_DEPRECATION -xobjective-c -fobjc-arc
#cgo LDFLAGS: -framework OpenGL
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
#include <CoreGraphics/CoreGraphics.h> #include <CoreGraphics/CoreGraphics.h>
#include <AppKit/AppKit.h> #include <AppKit/AppKit.h>
+2 -3
View File
@@ -2,13 +2,12 @@
// +build darwin,!ios,nometal // +build darwin,!ios,nometal
@import AppKit; #import <AppKit/AppKit.h>
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
#include <OpenGL/OpenGL.h> #include <OpenGL/OpenGL.h>
#include "_cgo_export.h" #include "_cgo_export.h"
CALayer *gio_layerFactory(void) { CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool { @autoreleasepool {
return [CALayer layer]; 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
}
+32 -10
View File
@@ -1,17 +1,16 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build go1.18
// +build go1.18
package app package app
import ( import (
"gioui.org/f32"
"testing" "testing"
"unicode/utf8" "unicode/utf8"
"gioui.org/font"
"gioui.org/font/gofont" "gioui.org/font/gofont"
"gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/router"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/text" "gioui.org/text"
@@ -28,17 +27,18 @@ func FuzzIME(f *testing.F) {
f.Add([]byte("20007800002\x02000")) f.Add([]byte("20007800002\x02000"))
f.Add([]byte("200A02000990\x19002\x17\x0200")) f.Add([]byte("200A02000990\x19002\x17\x0200"))
f.Fuzz(func(t *testing.T, cmds []byte) { f.Fuzz(func(t *testing.T, cmds []byte) {
cache := text.NewCache(gofont.Collection()) cache := text.NewShaper(text.WithCollection(gofont.Collection()))
e := new(widget.Editor) e := new(widget.Editor)
e.Focus()
var r router.Router var r input.Router
gtx := layout.Context{Ops: new(op.Ops), Queue: &r} gtx := layout.Context{Ops: new(op.Ops), Source: r.Source()}
gtx.Execute(key.FocusCmd{Tag: e})
// Layout once to register focus. // Layout once to register focus.
e.Layout(gtx, cache, text.Font{}, unit.Px(10), nil) e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
var state editorState var state editorState
state.Selection.Transform = f32.AffineId()
const ( const (
cmdReplace = iota cmdReplace = iota
cmdSelect cmdSelect
@@ -103,11 +103,32 @@ func FuzzIME(f *testing.F) {
} }
} }
cmds = cmds[cmdLen:] cmds = cmds[cmdLen:]
e.Layout(gtx, cache, text.Font{}, unit.Px(10), nil) e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
newState := r.EditorState() newState := r.EditorState()
// We don't track caret position. // We don't track caret position.
state.Selection.Caret = newState.Selection.Caret state.Selection.Caret = newState.Selection.Caret
// Expanded snippets are ok.
their, our := newState.Snippet, state.EditorState.Snippet
beforeLen := 0
for before := our.Start - their.Start; before > 0; before-- {
_, n := utf8.DecodeRuneInString(their.Text[beforeLen:])
beforeLen += n
}
afterLen := 0
for after := their.End - our.End; after > 0; after-- {
_, n := utf8.DecodeLastRuneInString(their.Text[:len(their.Text)-afterLen])
afterLen += n
}
if beforeLen > 0 {
our.Text = their.Text[:beforeLen] + our.Text
our.Start = their.Start
}
if afterLen > 0 {
our.Text = our.Text + their.Text[len(their.Text)-afterLen:]
our.End = their.End
}
state.EditorState.Snippet = our
if newState != state.EditorState { if newState != state.EditorState {
t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState) t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState)
} }
@@ -117,6 +138,7 @@ func FuzzIME(f *testing.F) {
func TestEditorIndices(t *testing.T) { func TestEditorIndices(t *testing.T) {
var s editorState var s editorState
s.Selection.Transform = f32.AffineId()
const str = "Hello, 😀" const str = "Hello, 😀"
s.Snippet = key.Snippet{ s.Snippet = key.Snippet{
Text: str, 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"
+243 -42
View File
@@ -47,6 +47,13 @@ type WndClassEx struct {
HIconSm syscall.Handle HIconSm syscall.Handle
} }
type Margins struct {
CxLeftWidth int32
CxRightWidth int32
CyTopHeight int32
CyBottomHeight int32
}
type Msg struct { type Msg struct {
Hwnd syscall.Handle Hwnd syscall.Handle
Message uint32 Message uint32
@@ -69,6 +76,21 @@ type MinMaxInfo struct {
PtMaxTrackSize Point PtMaxTrackSize Point
} }
type NCCalcSizeParams struct {
Rgrc [3]Rect
LpPos *WindowPos
}
type WindowPos struct {
HWND syscall.Handle
HWNDInsertAfter syscall.Handle
x int32
y int32
cx int32
cy int32
flags uint32
}
type WindowPlacement struct { type WindowPlacement struct {
length uint32 length uint32
flags uint32 flags uint32
@@ -86,6 +108,80 @@ type MonitorInfo struct {
Flags uint32 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 ( const (
TRUE = 1 TRUE = 1
@@ -111,9 +207,20 @@ const (
CFS_POINT = 0x0002 CFS_POINT = 0x0002
CFS_CANDIDATEPOS = 0x0040 CFS_CANDIDATEPOS = 0x0040
HWND_TOPMOST = ^(uint32(1) - 1) // -1 HWND_TOP = syscall.Handle(0)
HWND_TOPMOST = ^(syscall.Handle(1) - 1) // -1
HWND_NOTOPMOST = ^(syscall.Handle(2) - 1) // -2
HTCLIENT = 1 HTCAPTION = 2
HTCLIENT = 1
HTLEFT = 10
HTRIGHT = 11
HTTOP = 12
HTTOPLEFT = 13
HTTOPRIGHT = 14
HTBOTTOM = 15
HTBOTTOMLEFT = 16
HTBOTTOMRIGHT = 17
IDC_APPSTARTING = 32650 // Standard arrow and small hourglass IDC_APPSTARTING = 32650 // Standard arrow and small hourglass
IDC_ARROW = 32512 // Standard arrow IDC_ARROW = 32512 // Standard arrow
@@ -146,13 +253,16 @@ const (
SCS_SETSTR = GCS_COMPREADSTR | GCS_COMPSTR SCS_SETSTR = GCS_COMPREADSTR | GCS_COMPSTR
SM_CXSIZEFRAME = 32
SM_CYSIZEFRAME = 33
SW_SHOWDEFAULT = 10 SW_SHOWDEFAULT = 10
SW_SHOWMINIMIZED = 2 SW_SHOWMINIMIZED = 2
SW_SHOWMAXIMIZED = 3 SW_SHOWMAXIMIZED = 3
SW_SHOWNORMAL = 1 SW_SHOWNORMAL = 1
SW_SHOW = 5 SW_SHOW = 5
SWP_FRAMECHANGED = 0x0020
SWP_FRAMECHANGED = 0x0020
SWP_NOMOVE = 0x0002 SWP_NOMOVE = 0x0002
SWP_NOOWNERZORDER = 0x0200 SWP_NOOWNERZORDER = 0x0200
SWP_NOSIZE = 0x0001 SWP_NOSIZE = 0x0001
@@ -210,43 +320,54 @@ const (
UNICODE_NOCHAR = 65535 UNICODE_NOCHAR = 65535
WM_CANCELMODE = 0x001F WM_CANCELMODE = 0x001F
WM_CHAR = 0x0102 WM_CHAR = 0x0102
WM_CLOSE = 0x0010 WM_CLOSE = 0x0010
WM_CREATE = 0x0001 WM_COPYDATA = 0x004A
WM_DPICHANGED = 0x02E0 WM_CREATE = 0x0001
WM_DESTROY = 0x0002 WM_DPICHANGED = 0x02E0
WM_ERASEBKGND = 0x0014 WM_DESTROY = 0x0002
WM_GETMINMAXINFO = 0x0024 WM_ERASEBKGND = 0x0014
WM_IME_COMPOSITION = 0x010F WM_GETMINMAXINFO = 0x0024
WM_IME_ENDCOMPOSITION = 0x010E WM_IME_COMPOSITION = 0x010F
WM_IME_STARTCOMPOSITION = 0x010D WM_IME_ENDCOMPOSITION = 0x010E
WM_KEYDOWN = 0x0100 WM_IME_STARTCOMPOSITION = 0x010D
WM_KEYUP = 0x0101 WM_KEYDOWN = 0x0100
WM_KILLFOCUS = 0x0008 WM_KEYUP = 0x0101
WM_LBUTTONDOWN = 0x0201 WM_KILLFOCUS = 0x0008
WM_LBUTTONUP = 0x0202 WM_LBUTTONDOWN = 0x0201
WM_MBUTTONDOWN = 0x0207 WM_LBUTTONUP = 0x0202
WM_MBUTTONUP = 0x0208 WM_MBUTTONDOWN = 0x0207
WM_MOUSEMOVE = 0x0200 WM_MBUTTONUP = 0x0208
WM_MOUSEWHEEL = 0x020A WM_MOUSEMOVE = 0x0200
WM_MOUSEHWHEEL = 0x020E WM_MOUSEWHEEL = 0x020A
WM_PAINT = 0x000F WM_MOUSEHWHEEL = 0x020E
WM_QUIT = 0x0012 WM_NCACTIVATE = 0x0086
WM_SETCURSOR = 0x0020 WM_NCHITTEST = 0x0084
WM_SETFOCUS = 0x0007 WM_NCCALCSIZE = 0x0083
WM_SHOWWINDOW = 0x0018 WM_PAINT = 0x000F
WM_SIZE = 0x0005 WM_POINTERCAPTURECHANGED = 0x024C
WM_SYSKEYDOWN = 0x0104 WM_POINTERDOWN = 0x0246
WM_SYSKEYUP = 0x0105 WM_POINTERUP = 0x0247
WM_RBUTTONDOWN = 0x0204 WM_POINTERUPDATE = 0x0245
WM_RBUTTONUP = 0x0205 WM_POINTERWHEEL = 0x024E
WM_TIMER = 0x0113 WM_POINTERHWHEEL = 0x024F
WM_UNICHAR = 0x0109 WM_QUIT = 0x0012
WM_USER = 0x0400 WM_RBUTTONDOWN = 0x0204
WM_WINDOWPOSCHANGED = 0x0047 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 = 0x00010000 WS_CLIPCHILDREN = 0x02000000
WS_CLIPSIBLINGS = 0x04000000 WS_CLIPSIBLINGS = 0x04000000
WS_MAXIMIZE = 0x01000000 WS_MAXIMIZE = 0x01000000
WS_ICONIC = 0x20000000 WS_ICONIC = 0x20000000
@@ -307,8 +428,11 @@ var (
_DefWindowProc = user32.NewProc("DefWindowProcW") _DefWindowProc = user32.NewProc("DefWindowProcW")
_DestroyWindow = user32.NewProc("DestroyWindow") _DestroyWindow = user32.NewProc("DestroyWindow")
_DispatchMessage = user32.NewProc("DispatchMessageW") _DispatchMessage = user32.NewProc("DispatchMessageW")
_FindWindow = user32.NewProc("FindWindowW")
_EmptyClipboard = user32.NewProc("EmptyClipboard") _EmptyClipboard = user32.NewProc("EmptyClipboard")
_EnableMouseInPointer = user32.NewProc("EnableMouseInPointer")
_GetWindowRect = user32.NewProc("GetWindowRect") _GetWindowRect = user32.NewProc("GetWindowRect")
_GetClientRect = user32.NewProc("GetClientRect")
_GetClipboardData = user32.NewProc("GetClipboardData") _GetClipboardData = user32.NewProc("GetClipboardData")
_GetDC = user32.NewProc("GetDC") _GetDC = user32.NewProc("GetDC")
_GetDpiForWindow = user32.NewProc("GetDpiForWindow") _GetDpiForWindow = user32.NewProc("GetDpiForWindow")
@@ -316,6 +440,8 @@ var (
_GetMessage = user32.NewProc("GetMessageW") _GetMessage = user32.NewProc("GetMessageW")
_GetMessageTime = user32.NewProc("GetMessageTime") _GetMessageTime = user32.NewProc("GetMessageTime")
_GetMonitorInfo = user32.NewProc("GetMonitorInfoW") _GetMonitorInfo = user32.NewProc("GetMonitorInfoW")
_GetPointerInfo = user32.NewProc("GetPointerInfo")
_GetSystemMetrics = user32.NewProc("GetSystemMetrics")
_GetWindowLong = user32.NewProc("GetWindowLongPtrW") _GetWindowLong = user32.NewProc("GetWindowLongPtrW")
_GetWindowLong32 = user32.NewProc("GetWindowLongW") _GetWindowLong32 = user32.NewProc("GetWindowLongW")
_GetWindowPlacement = user32.NewProc("GetWindowPlacement") _GetWindowPlacement = user32.NewProc("GetWindowPlacement")
@@ -332,9 +458,11 @@ var (
_PostQuitMessage = user32.NewProc("PostQuitMessage") _PostQuitMessage = user32.NewProc("PostQuitMessage")
_ReleaseCapture = user32.NewProc("ReleaseCapture") _ReleaseCapture = user32.NewProc("ReleaseCapture")
_RegisterClassExW = user32.NewProc("RegisterClassExW") _RegisterClassExW = user32.NewProc("RegisterClassExW")
_RegisterTouchWindow = user32.NewProc("RegisterTouchWindow")
_ReleaseDC = user32.NewProc("ReleaseDC") _ReleaseDC = user32.NewProc("ReleaseDC")
_ScreenToClient = user32.NewProc("ScreenToClient") _ScreenToClient = user32.NewProc("ScreenToClient")
_ShowWindow = user32.NewProc("ShowWindow") _ShowWindow = user32.NewProc("ShowWindow")
_SendMessage = user32.NewProc("SendMessageW")
_SetCapture = user32.NewProc("SetCapture") _SetCapture = user32.NewProc("SetCapture")
_SetCursor = user32.NewProc("SetCursor") _SetCursor = user32.NewProc("SetCursor")
_SetClipboardData = user32.NewProc("SetClipboardData") _SetClipboardData = user32.NewProc("SetClipboardData")
@@ -364,6 +492,9 @@ var (
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext") _ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow") _ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow") _ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
dwmapi = syscall.NewLazySystemDLL("dwmapi")
_DwmExtendFrameIntoClientArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
) )
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) { func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
@@ -384,7 +515,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) { 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( hwnd, _, err := _CreateWindowEx.Call(
uintptr(dwExStyle), uintptr(dwExStyle),
uintptr(lpClassName), uintptr(lpClassName),
@@ -402,6 +536,31 @@ func CreateWindowEx(dwExStyle uint32, lpClassName uint16, lpWindowName string, d
return syscall.Handle(hwnd), nil 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 { func DefWindowProc(hwnd syscall.Handle, msg uint32, wparam, lparam uintptr) uintptr {
r, _, _ := _DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam) r, _, _ := _DefWindowProc.Call(uintptr(hwnd), uintptr(msg), wparam, lparam)
return r return r
@@ -415,6 +574,14 @@ func DispatchMessage(m *Msg) {
_DispatchMessage.Call(uintptr(unsafe.Pointer(m))) _DispatchMessage.Call(uintptr(unsafe.Pointer(m)))
} }
func DwmExtendFrameIntoClientArea(hwnd syscall.Handle, margins Margins) error {
r, _, _ := _DwmExtendFrameIntoClientArea.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&margins)))
if r != 0 {
return fmt.Errorf("DwmExtendFrameIntoClientArea: %#x", r)
}
return nil
}
func EmptyClipboard() error { func EmptyClipboard() error {
r, _, err := _EmptyClipboard.Call() r, _, err := _EmptyClipboard.Call()
if r == 0 { if r == 0 {
@@ -423,12 +590,30 @@ func EmptyClipboard() error {
return nil 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 { func GetWindowRect(hwnd syscall.Handle) Rect {
var r Rect var r Rect
_GetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r))) _GetWindowRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r)))
return r return r
} }
func GetClientRect(hwnd syscall.Handle) Rect {
var r Rect
_GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r)))
return r
}
func GetClipboardData(format uint32) (syscall.Handle, error) { func GetClipboardData(format uint32) (syscall.Handle, error) {
r, _, err := _GetClipboardData.Call(uintptr(format)) r, _, err := _GetClipboardData.Call(uintptr(format))
if r == 0 { if r == 0 {
@@ -499,6 +684,11 @@ func GetMessageTime() time.Duration {
return time.Duration(r) * time.Millisecond return time.Duration(r) * time.Millisecond
} }
func GetSystemMetrics(nIndex int) int {
r, _, _ := _GetSystemMetrics.Call(uintptr(nIndex))
return int(r)
}
// GetWindowDPI returns the effective DPI of the window. // GetWindowDPI returns the effective DPI of the window.
func GetWindowDPI(hwnd syscall.Handle) int { func GetWindowDPI(hwnd syscall.Handle) int {
// Check for GetDpiForWindow, introduced in Windows 10. // Check for GetDpiForWindow, introduced in Windows 10.
@@ -594,7 +784,7 @@ func SetWindowPlacement(hwnd syscall.Handle, wp *WindowPlacement) {
_SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp))) _SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp)))
} }
func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int32, style uintptr) { func SetWindowPos(hwnd, hwndInsertAfter syscall.Handle, x, y, dx, dy int32, style uintptr) {
_SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter), _SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter),
uintptr(x), uintptr(y), uintptr(x), uintptr(y),
uintptr(dx), uintptr(dy), uintptr(dx), uintptr(dy),
@@ -603,7 +793,10 @@ func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int3
} }
func SetWindowText(hwnd syscall.Handle, title string) { 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))) _SetWindowText.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wname)))
} }
@@ -719,6 +912,14 @@ func ReleaseDC(hdc syscall.Handle) {
_ReleaseDC.Call(uintptr(hdc)) _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) { func SetForegroundWindow(hwnd syscall.Handle) {
_SetForegroundWindow.Call(uintptr(hwnd)) _SetForegroundWindow.Call(uintptr(hwnd))
} }
+74 -8
View File
@@ -136,6 +136,10 @@ func (x *Context) LoadKeymap(format int, fd int, size int) error {
func (x *Context) Modifiers() key.Modifiers { func (x *Context) Modifiers() key.Modifiers {
var mods key.Modifiers var mods key.Modifiers
if x.state == nil {
return mods
}
if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 { if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 {
mods |= key.ModCtrl mods |= key.ModCtrl
} }
@@ -219,6 +223,9 @@ func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte {
} }
func (x *Context) IsRepeatKey(keyCode uint32) bool { func (x *Context) IsRepeatKey(keyCode uint32) bool {
if x.state == nil {
return false
}
kc := C.xkb_keycode_t(keyCode) kc := C.xkb_keycode_t(keyCode)
return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1 return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1
} }
@@ -231,14 +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)) 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' { 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 key.Name(rune(s - C.XKB_KEY_KP_0 + '0')), true
} }
if ' ' < s && s <= '~' { if ' ' < s && s <= '~' {
return string(rune(s)), true return key.Name(rune(s)), true
} }
var n string var n key.Name
switch s { switch s {
case C.XKB_KEY_Escape: case C.XKB_KEY_Escape:
n = key.NameEscape n = key.NameEscape
@@ -248,8 +258,6 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
n = key.NameRightArrow n = key.NameRightArrow
case C.XKB_KEY_Return: case C.XKB_KEY_Return:
n = key.NameReturn n = key.NameReturn
case C.XKB_KEY_KP_Enter:
n = key.NameEnter
case C.XKB_KEY_Up: case C.XKB_KEY_Up:
n = key.NameUpArrow n = key.NameUpArrow
case C.XKB_KEY_Down: case C.XKB_KEY_Down:
@@ -290,9 +298,9 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
n = key.NameF11 n = key.NameF11
case C.XKB_KEY_F12: case C.XKB_KEY_F12:
n = key.NameF12 n = key.NameF12
case C.XKB_KEY_Tab, C.XKB_KEY_KP_Tab, C.XKB_KEY_ISO_Left_Tab: case C.XKB_KEY_Tab, C.XKB_KEY_ISO_Left_Tab:
n = key.NameTab n = key.NameTab
case 0x20, C.XKB_KEY_KP_Space: case 0x20:
n = key.NameSpace n = key.NameSpace
case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R: case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R:
n = key.NameCtrl n = key.NameCtrl
@@ -302,6 +310,64 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
n = key.NameAlt n = key.NameAlt
case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R: case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R:
n = key.NameSuper n = key.NameSuper
case C.XKB_KEY_KP_Space:
n = key.NameSpace
case C.XKB_KEY_KP_Tab:
n = key.NameTab
case C.XKB_KEY_KP_Enter:
n = key.NameEnter
case C.XKB_KEY_KP_F1:
n = key.NameF1
case C.XKB_KEY_KP_F2:
n = key.NameF2
case C.XKB_KEY_KP_F3:
n = key.NameF3
case C.XKB_KEY_KP_F4:
n = key.NameF4
case C.XKB_KEY_KP_Home:
n = key.NameHome
case C.XKB_KEY_KP_Left:
n = key.NameLeftArrow
case C.XKB_KEY_KP_Up:
n = key.NameUpArrow
case C.XKB_KEY_KP_Right:
n = key.NameRightArrow
case C.XKB_KEY_KP_Down:
n = key.NameDownArrow
case C.XKB_KEY_KP_Prior:
// not supported
return "", false
case C.XKB_KEY_KP_Next:
// not supported
return "", false
case C.XKB_KEY_KP_End:
n = key.NameEnd
case C.XKB_KEY_KP_Begin:
n = key.NameHome
case C.XKB_KEY_KP_Insert:
// not supported
return "", false
case C.XKB_KEY_KP_Delete:
n = key.NameDeleteForward
case C.XKB_KEY_KP_Multiply:
n = "*"
case C.XKB_KEY_KP_Add:
n = "+"
case C.XKB_KEY_KP_Separator:
// not supported
return "", false
case C.XKB_KEY_KP_Subtract:
n = "-"
case C.XKB_KEY_KP_Decimal:
// TODO(dh): does a German keyboard layout also translate the numpad key to XKB_KEY_KP_DECIMAL? Because in
// German, the decimal is a comma, not a period.
n = "."
case C.XKB_KEY_KP_Divide:
n = "/"
case C.XKB_KEY_KP_Equal:
n = "="
default: default:
return "", false return "", false
} }
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
package log package app
/* /*
#cgo LDFLAGS: -llog #cgo LDFLAGS: -llog
@@ -22,7 +22,7 @@ import (
// 1024 is the truncation limit from android/log.h, plus a \n. // 1024 is the truncation limit from android/log.h, plus a \n.
const logLineLimit = 1024 const logLineLimit = 1024
var logTag = C.CString(appID) var logTag = C.CString(ID)
func init() { func init() {
// Android's logcat already includes timestamps. // Android's logcat already includes timestamps.
@@ -1,9 +1,8 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build darwin && ios //go:build darwin && ios
// +build darwin,ios
package log package app
/* /*
#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c #cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c
@@ -1,17 +1,18 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
package log package app
import ( import (
"log" "log"
"syscall"
"unsafe" "unsafe"
syscall "golang.org/x/sys/windows"
) )
type logger struct{} type logger struct{}
var ( var (
kernel32 = syscall.NewLazyDLL("kernel32") kernel32 = syscall.NewLazySystemDLL("kernel32")
outputDebugStringW = kernel32.NewProc("OutputDebugStringW") outputDebugStringW = kernel32.NewProc("OutputDebugStringW")
debugView *logger debugView *logger
) )
+7 -5
View File
@@ -12,10 +12,11 @@ import (
) )
/* /*
#cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc #cgo CFLAGS: -Werror -xobjective-c -fobjc-arc
#cgo LDFLAGS: -framework QuartzCore -framework Metal
@import Metal; #import <Metal/Metal.h>
@import QuartzCore.CAMetalLayer; #import <QuartzCore/CAMetalLayer.h>
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
@@ -59,8 +60,9 @@ static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) {
id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef; id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef; id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer]; id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
[cmdBuffer presentDrawable:drawable];
[cmdBuffer commit]; [cmdBuffer commit];
[cmdBuffer waitUntilScheduled];
[drawable present];
} }
} }
@@ -94,7 +96,7 @@ func newMtlContext(w *window) (*mtlContext, error) {
return nil, errors.New("metal: CAMetalLayer construction failed") return nil, errors.New("metal: CAMetalLayer construction failed")
} }
queue := C.newCommandQueue(dev) queue := C.newCommandQueue(dev)
if layer == 0 { if queue == 0 {
C.CFRelease(dev) C.CFRelease(dev)
C.CFRelease(layer) C.CFRelease(layer)
return nil, errors.New("metal: [MTLDevice newCommandQueue] failed") return nil, errors.New("metal: [MTLDevice newCommandQueue] failed")
+4 -2
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build !nometal //go:build !nometal
// +build !nometal
package app package app
@@ -21,7 +20,10 @@ Class gio_layerClass(void) {
static CFTypeRef getMetalLayer(CFTypeRef viewRef) { static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
@autoreleasepool { @autoreleasepool {
UIView *view = (__bridge UIView *)viewRef; UIView *view = (__bridge UIView *)viewRef;
return CFBridgingRetain(view.layer); CAMetalLayer *l = (CAMetalLayer *)view.layer;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = YES;
return CFBridgingRetain(l);
} }
} }
+9 -7
View File
@@ -6,17 +6,19 @@
package app package app
/* /*
#cgo CFLAGS: -Werror -xobjective-c -fmodules -fobjc-arc #cgo CFLAGS: -Werror -xobjective-c -fobjc-arc
@import AppKit;
@import QuartzCore.CAMetalLayer;
#import <AppKit/AppKit.h>
#import <QuartzCore/CAMetalLayer.h>
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
CALayer *gio_layerFactory(void) { CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool { @autoreleasepool {
return [CAMetalLayer layer]; CAMetalLayer *l = [CAMetalLayer layer];
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = presentWithTrans;
return l;
} }
} }
+169 -28
View File
@@ -7,7 +7,9 @@ import (
"image" "image"
"image/color" "image/color"
"gioui.org/io/event"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/op"
"gioui.org/gpu" "gioui.org/gpu"
"gioui.org/io/pointer" "gioui.org/io/pointer"
@@ -41,10 +43,15 @@ type Config struct {
// CustomRenderer is true when the window content is rendered by the // CustomRenderer is true when the window content is rendered by the
// client. // client.
CustomRenderer bool CustomRenderer bool
// center is a flag used to center the window. Set by option.
center bool
// Decorated reports whether window decorations are provided automatically. // Decorated reports whether window decorations are provided automatically.
Decorated bool 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
} }
// ConfigEvent is sent whenever the configuration of a Window changes. // ConfigEvent is sent whenever the configuration of a Window changes.
@@ -130,8 +137,30 @@ func (o Orientation) String() string {
return "" 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 { type frameEvent struct {
system.FrameEvent FrameEvent
Sync bool Sync bool
} }
@@ -146,9 +175,13 @@ type context interface {
Unlock() Unlock()
} }
// Driver is the interface for the platform implementation // driver is the interface for the platform implementation
// of a window. // of a window.
type driver interface { 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, // SetAnimating sets the animation flag. When the window is animating,
// FrameEvents are delivered as fast as the display can handle them. // FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool) SetAnimating(anim bool)
@@ -159,27 +192,27 @@ type driver interface {
// ReadClipboard requests the clipboard content. // ReadClipboard requests the clipboard content.
ReadClipboard() ReadClipboard()
// WriteClipboard requests a clipboard write. // WriteClipboard requests a clipboard write.
WriteClipboard(s string) WriteClipboard(mime string, s []byte)
// Configure the window. // Configure the window.
Configure([]Option) Configure([]Option)
// SetCursor updates the current cursor to name. // SetCursor updates the current cursor to name.
SetCursor(cursor pointer.Cursor) SetCursor(cursor pointer.Cursor)
// Raise the window at the top.
Raise()
// Close the window.
Close()
// Wakeup wakes up the event loop and sends a WakeupEvent.
Wakeup()
// Perform actions on the window. // Perform actions on the window.
Perform(system.Action) Perform(system.Action)
// EditorStateChanged notifies the driver that the editor state changed. // EditorStateChanged notifies the driver that the editor state changed.
EditorStateChanged(old, new editorState) 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 { type windowRendezvous struct {
in chan windowAndConfig in chan windowAndConfig
out chan windowAndConfig out chan windowAndConfig
errs chan error windows chan struct{}
} }
type windowAndConfig struct { type windowAndConfig struct {
@@ -189,32 +222,137 @@ type windowAndConfig struct {
func newWindowRendezvous() *windowRendezvous { func newWindowRendezvous() *windowRendezvous {
wr := &windowRendezvous{ wr := &windowRendezvous{
in: make(chan windowAndConfig), in: make(chan windowAndConfig),
out: make(chan windowAndConfig), out: make(chan windowAndConfig),
errs: make(chan error), windows: make(chan struct{}),
} }
go func() { go func() {
var main windowAndConfig in := wr.in
var window windowAndConfig
var out chan windowAndConfig var out chan windowAndConfig
for { for {
select { select {
case w := <-wr.in: case w := <-in:
var err error window = w
if main.window != nil {
err = errors.New("multiple windows are not supported")
}
wr.errs <- err
main = w
out = wr.out out = wr.out
case out <- main: case out <- window:
} }
} }
}() }()
return wr return wr
} }
func (wakeupEvent) ImplementsEvent() {} func newEventLoop(w *callbacks, wakeup func()) *eventLoop {
func (ConfigEvent) ImplementsEvent() {} 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)) { func walkActions(actions system.Action, do func(system.Action)) {
for a := system.Action(1); actions != 0; a <<= 1 { for a := system.Action(1); actions != 0; a <<= 1 {
@@ -224,3 +362,6 @@ func walkActions(actions system.Action, do func(system.Action)) {
} }
} }
} }
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
+239 -165
View File
@@ -123,24 +123,29 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"io"
"math" "math"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"runtime/cgo" "runtime/cgo"
"runtime/debug" "runtime/debug"
"strings"
"sync" "sync"
"time" "time"
"unicode/utf16" "unicode/utf16"
"unsafe" "unsafe"
"gioui.org/io/transfer"
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard" "gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/unit" "gioui.org/unit"
@@ -148,26 +153,28 @@ import (
type window struct { type window struct {
callbacks *callbacks callbacks *callbacks
loop *eventLoop
view C.jobject view C.jobject
handle cgo.Handle handle cgo.Handle
dpi int dpi int
fontScale float32 fontScale float32
insets system.Insets insets pixelInsets
stage system.Stage visible bool
started bool started bool
animating bool animating bool
win *C.ANativeWindow win *C.ANativeWindow
config Config config Config
inputHint key.InputHint
semantic struct { semantic struct {
hoverID router.SemanticID hoverID input.SemanticID
rootID router.SemanticID rootID input.SemanticID
focusID router.SemanticID focusID input.SemanticID
diffs []router.SemanticID diffs []input.SemanticID
} }
} }
@@ -195,9 +202,13 @@ var gioView struct {
updateCaret C.jmethodID updateCaret C.jmethodID
} }
// ViewEvent is sent whenever the Window's underlying Android view type pixelInsets struct {
top, bottom, left, right int
}
// AndroidViewEvent is sent whenever the Window's underlying Android view
// changes. // changes.
type ViewEvent struct { type AndroidViewEvent struct {
// View is a JNI global reference to the android.view.View // View is a JNI global reference to the android.view.View
// instance backing the Window. The reference is valid until // instance backing the Window. The reference is valid until
// the next ViewEvent is received. // the next ViewEvent is received.
@@ -207,8 +218,6 @@ type ViewEvent struct {
type jvalue uint64 // The largest JNI type fits in 64 bits. type jvalue uint64 // The largest JNI type fits in 64 bits.
var dataDirChan = make(chan string, 1)
var android struct { var android struct {
// mu protects all fields of this structure. However, once a // mu protects all fields of this structure. However, once a
// non-nil jvm is returned from javaVM, all the other fields may // non-nil jvm is returned from javaVM, all the other fields may
@@ -284,8 +293,7 @@ var mainWindow = newWindowRendezvous()
var mainFuncs = make(chan func(env *C.JNIEnv), 1) var mainFuncs = make(chan func(env *C.JNIEnv), 1)
var ( var (
dataDirOnce sync.Once dataPath string
dataPath string
) )
var ( var (
@@ -310,7 +318,7 @@ const (
) )
func (w *window) NewContext() (context, error) { func (w *window) NewContext() (context, error) {
funcs := []func(w *window) (context, error){newAndroidVulkanContext, newAndroidGLESContext} funcs := []func(w *window) (context, error){newAndroidGLESContext, newAndroidVulkanContext}
var firstErr error var firstErr error
for _, f := range funcs { for _, f := range funcs {
if f == nil { if f == nil {
@@ -332,23 +340,9 @@ func (w *window) NewContext() (context, error) {
} }
func dataDir() (string, error) { func dataDir() (string, error) {
dataDirOnce.Do(func() { if dataPath == "" {
dataPath = <-dataDirChan panic("DataDir isn't valid before main")
// Set XDG_CACHE_HOME to make os.UserCacheDir work. }
if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists {
cachePath := filepath.Join(dataPath, "cache")
os.Setenv("XDG_CACHE_HOME", cachePath)
}
// Set XDG_CONFIG_HOME to make os.UserConfigDir work.
if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists {
cfgPath := filepath.Join(dataPath, "config")
os.Setenv("XDG_CONFIG_HOME", cfgPath)
}
// Set HOME to make os.UserHomeDir work.
if _, exists := os.LookupEnv("HOME"); !exists {
os.Setenv("HOME", dataPath)
}
})
return dataPath, nil return dataPath, nil
} }
@@ -385,7 +379,23 @@ func Java_org_gioui_Gio_runGoMain(env *C.JNIEnv, class C.jclass, jdataDir C.jbyt
} }
n := C.jni_GetArrayLength(env, jdataDir) n := C.jni_GetArrayLength(env, jdataDir)
dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n) dataDir := C.GoStringN((*C.char)(unsafe.Pointer(dirBytes)), n)
dataDirChan <- dataDir
// Set XDG_CACHE_HOME to make os.UserCacheDir work.
if _, exists := os.LookupEnv("XDG_CACHE_HOME"); !exists {
cachePath := filepath.Join(dataDir, "cache")
os.Setenv("XDG_CACHE_HOME", cachePath)
}
// Set XDG_CONFIG_HOME to make os.UserConfigDir work.
if _, exists := os.LookupEnv("XDG_CONFIG_HOME"); !exists {
cfgPath := filepath.Join(dataDir, "config")
os.Setenv("XDG_CONFIG_HOME", cfgPath)
}
// Set HOME to make os.UserHomeDir work.
if _, exists := os.LookupEnv("HOME"); !exists {
os.Setenv("HOME", dataDir)
}
dataPath = dataDir
C.jni_ReleaseByteArrayElements(env, jdataDir, dirBytes) C.jni_ReleaseByteArrayElements(env, jdataDir, dirBytes)
runMain() runMain()
@@ -479,24 +489,30 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
}) })
view = C.jni_NewGlobalRef(env, view) view = C.jni_NewGlobalRef(env, view)
wopts := <-mainWindow.out wopts := <-mainWindow.out
var cnf Config
w, ok := windows[wopts.window] w, ok := windows[wopts.window]
if !ok { if !ok {
w = &window{ w = &window{
callbacks: wopts.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 windows[wopts.window] = w
} else {
cnf = w.config
} }
mainWindow.windows <- struct{}{}
if w.view != 0 { if w.view != 0 {
w.detach(env) w.detach(env)
} }
w.view = view w.view = view
w.visible = false
w.handle = cgo.NewHandle(w) w.handle = cgo.NewHandle(w)
w.callbacks.SetDriver(w)
w.loadConfig(env, class) w.loadConfig(env, class)
w.Configure(wopts.options) w.setConfig(env, cnf)
w.SetInputHint(key.HintAny) w.SetInputHint(w.inputHint)
w.setStage(system.StagePaused) w.processEvent(AndroidViewEvent{View: uintptr(view)})
w.callbacks.Event(ViewEvent{View: uintptr(view)})
return C.jlong(w.handle) return C.jlong(w.handle)
} }
@@ -510,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) { func Java_org_gioui_GioView_onStopView(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := cgo.Handle(handle).Value().(*window) w := cgo.Handle(handle).Value().(*window)
w.started = false w.started = false
w.setStage(system.StagePaused) w.visible = false
} }
//export Java_org_gioui_GioView_onStartView //export Java_org_gioui_GioView_onStartView
@@ -526,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) { func Java_org_gioui_GioView_onSurfaceDestroyed(env *C.JNIEnv, class C.jclass, handle C.jlong) {
w := cgo.Handle(handle).Value().(*window) w := cgo.Handle(handle).Value().(*window)
w.win = nil w.win = nil
w.setStage(system.StagePaused) w.visible = false
} }
//export Java_org_gioui_GioView_onSurfaceChanged //export Java_org_gioui_GioView_onSurfaceChanged
@@ -548,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) { func Java_org_gioui_GioView_onConfigurationChanged(env *C.JNIEnv, class C.jclass, view C.jlong) {
w := cgo.Handle(view).Value().(*window) w := cgo.Handle(view).Value().(*window)
w.loadConfig(env, class) w.loadConfig(env, class)
if w.stage >= system.StageRunning { w.draw(env, true)
w.draw(env, true)
}
} }
//export Java_org_gioui_GioView_onFrameCallback //export Java_org_gioui_GioView_onFrameCallback
@@ -559,21 +573,13 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
if !exist { if !exist {
return return
} }
if w.stage < system.StageRunning { w.draw(env, false)
return
}
if w.animating {
w.draw(env, false)
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
} }
//export Java_org_gioui_GioView_onBack //export Java_org_gioui_GioView_onBack
func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean { func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean {
w := cgo.Handle(view).Value().(*window) w := cgo.Handle(view).Value().(*window)
ev := &system.CommandEvent{Type: system.CommandBack} if w.processEvent(key.Event{Name: key.NameBack}) {
w.callbacks.Event(ev)
if ev.Cancel {
return C.JNI_TRUE return C.JNI_TRUE
} }
return C.JNI_FALSE return C.JNI_FALSE
@@ -582,21 +588,20 @@ func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong)
//export Java_org_gioui_GioView_onFocusChange //export Java_org_gioui_GioView_onFocusChange
func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) { 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 := 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 //export Java_org_gioui_GioView_onWindowInsets
func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C.jlong, top, right, bottom, left C.jint) { func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C.jlong, top, right, bottom, left C.jint) {
w := cgo.Handle(view).Value().(*window) w := cgo.Handle(view).Value().(*window)
w.insets = system.Insets{ w.insets = pixelInsets{
Top: unit.Px(float32(top)), top: int(top),
Bottom: unit.Px(float32(bottom)), bottom: int(bottom),
Left: unit.Px(float32(left)), left: int(left),
Right: unit.Px(float32(right)), right: int(right),
}
if w.stage >= system.StageRunning {
w.draw(env, true)
} }
w.draw(env, true)
} }
//export Java_org_gioui_GioView_initializeAccessibilityNodeInfo //export Java_org_gioui_GioView_initializeAccessibilityNodeInfo
@@ -657,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 { for _, ch := range sem.Children {
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID))) err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))
if err != nil { if err != nil {
@@ -700,7 +742,7 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNod
panic(err) panic(err)
} }
} }
if d.Gestures&router.ClickGesture != 0 { if d.Gestures&input.ClickGesture != 0 {
addAction(ACTION_CLICK) addAction(ACTION_CLICK)
} }
clsName := android.strings.androidViewView clsName := android.strings.androidViewView
@@ -745,25 +787,23 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNod
return nil return nil
} }
func (w *window) virtualIDFor(id router.SemanticID) C.jint { func (w *window) virtualIDFor(id input.SemanticID) C.jint {
// TODO: Android virtual IDs are 32-bit Java integers, but childID is a int64.
if id == w.semantic.rootID { if id == w.semantic.rootID {
return HOST_VIEW_ID return HOST_VIEW_ID
} }
return C.jint(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 { if virtID == HOST_VIEW_ID {
return w.semantic.rootID return w.semantic.rootID
} }
return router.SemanticID(virtID) return input.SemanticID(virtID)
} }
func (w *window) detach(env *C.JNIEnv) { func (w *window) detach(env *C.JNIEnv) {
callVoidMethod(env, w.view, gioView.unregister) callVoidMethod(env, w.view, gioView.unregister)
w.callbacks.Event(ViewEvent{}) w.processEvent(AndroidViewEvent{})
w.callbacks.SetDriver(nil)
w.handle.Delete() w.handle.Delete()
C.jni_DeleteGlobalRef(env, w.view) C.jni_DeleteGlobalRef(env, w.view)
w.view = 0 w.view = 0
@@ -774,18 +814,10 @@ func (w *window) setVisible(env *C.JNIEnv) {
if width == 0 || height == 0 { if width == 0 || height == 0 {
return return
} }
w.setStage(system.StageRunning) w.visible = true
w.draw(env, 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 { func (w *window) setVisual(visID int) error {
if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 { if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 {
return errors.New("ANativeWindow_setBuffersGeometry failed") return errors.New("ANativeWindow_setBuffersGeometry failed")
@@ -822,21 +854,31 @@ func (w *window) SetAnimating(anim bool) {
} }
func (w *window) draw(env *C.JNIEnv, sync 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))) size := image.Pt(int(C.ANativeWindow_getWidth(w.win)), int(C.ANativeWindow_getHeight(w.win)))
if size != w.config.Size { if size != w.config.Size {
w.config.Size = 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 { if size.X == 0 || size.Y == 0 {
return return
} }
const inchPrDp = 1.0 / 160 const inchPrDp = 1.0 / 160
ppdp := float32(w.dpi) * inchPrDp ppdp := float32(w.dpi) * inchPrDp
w.callbacks.Event(frameEvent{ dppp := unit.Dp(1.0 / ppdp)
FrameEvent: system.FrameEvent{ insets := Insets{
Top: unit.Dp(w.insets.top) * dppp,
Bottom: unit.Dp(w.insets.bottom) * dppp,
Left: unit.Dp(w.insets.left) * dppp,
Right: unit.Dp(w.insets.right) * dppp,
}
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Insets: w.insets, Insets: insets,
Metric: unit.Metric{ Metric: unit.Metric{
PxPerDp: ppdp, PxPerDp: ppdp,
PxPerSp: w.fontScale * ppdp, PxPerSp: w.fontScale * ppdp,
@@ -844,6 +886,9 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
}, },
Sync: sync, Sync: sync,
}) })
if w.animating {
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
a11yActive, err := callBooleanMethod(env, w.view, gioView.isA11yActive) a11yActive, err := callBooleanMethod(env, w.view, gioView.isA11yActive)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -867,8 +912,6 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
} }
} }
type keyMapper func(devId, keyCode C.int32_t) rune
func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) { func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
if jvm == nil { if jvm == nil {
panic("nil JVM") panic("nil JVM")
@@ -889,8 +932,8 @@ func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
f(env) f(env)
} }
func convertKeyCode(code C.jint) (string, bool) { func convertKeyCode(code C.jint) (key.Name, bool) {
var n string var n key.Name
switch code { switch code {
case C.AKEYCODE_FORWARD_DEL: case C.AKEYCODE_FORWARD_DEL:
n = key.NameDeleteForward n = key.NameDeleteForward
@@ -908,6 +951,14 @@ func convertKeyCode(code C.jint) (string, bool) {
n = key.NameAlt n = key.NameAlt
case C.AKEYCODE_META_LEFT, C.AKEYCODE_META_RIGHT: case C.AKEYCODE_META_LEFT, C.AKEYCODE_META_RIGHT:
n = key.NameSuper n = key.NameSuper
case C.AKEYCODE_DPAD_UP:
n = key.NameUpArrow
case C.AKEYCODE_DPAD_DOWN:
n = key.NameDownArrow
case C.AKEYCODE_DPAD_LEFT:
n = key.NameLeftArrow
case C.AKEYCODE_DPAD_RIGHT:
n = key.NameRightArrow
default: default:
return "", false return "", false
} }
@@ -917,26 +968,16 @@ func convertKeyCode(code C.jint) (string, bool) {
//export Java_org_gioui_GioView_onKeyEvent //export Java_org_gioui_GioView_onKeyEvent
func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, keyCode, r C.jint, pressed C.jboolean, t C.jlong) { func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, keyCode, r C.jint, pressed C.jboolean, t C.jlong) {
w := cgo.Handle(handle).Value().(*window) w := cgo.Handle(handle).Value().(*window)
if pressed == C.JNI_TRUE { if pressed == C.JNI_TRUE && keyCode == C.AKEYCODE_DPAD_CENTER {
switch keyCode { w.callbacks.ClickFocus()
case C.AKEYCODE_DPAD_UP: return
w.callbacks.MoveFocus(router.FocusUp)
case C.AKEYCODE_DPAD_DOWN:
w.callbacks.MoveFocus(router.FocusDown)
case C.AKEYCODE_DPAD_LEFT:
w.callbacks.MoveFocus(router.FocusLeft)
case C.AKEYCODE_DPAD_RIGHT:
w.callbacks.MoveFocus(router.FocusRight)
case C.AKEYCODE_DPAD_CENTER:
w.callbacks.ClickFocus()
}
} }
if n, ok := convertKeyCode(keyCode); ok { if n, ok := convertKeyCode(keyCode); ok {
state := key.Release state := key.Release
if pressed == C.JNI_TRUE { if pressed == C.JNI_TRUE {
state = key.Press 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). 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))) w.callbacks.EditorInsert(string(rune(r)))
@@ -946,18 +987,18 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
//export Java_org_gioui_GioView_onTouchEvent //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) { 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) w := cgo.Handle(handle).Value().(*window)
var typ pointer.Type var kind pointer.Kind
switch action { switch action {
case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN: 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: case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP:
typ = pointer.Release kind = pointer.Release
case C.AMOTION_EVENT_ACTION_CANCEL: case C.AMOTION_EVENT_ACTION_CANCEL:
typ = pointer.Cancel kind = pointer.Cancel
case C.AMOTION_EVENT_ACTION_MOVE: case C.AMOTION_EVENT_ACTION_MOVE:
typ = pointer.Move kind = pointer.Move
case C.AMOTION_EVENT_ACTION_SCROLL: case C.AMOTION_EVENT_ACTION_SCROLL:
typ = pointer.Scroll kind = pointer.Scroll
default: default:
return return
} }
@@ -986,8 +1027,8 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
default: default:
return return
} }
w.callbacks.Event(pointer.Event{ w.processEvent(pointer.Event{
Type: typ, Kind: kind,
Source: src, Source: src,
Buttons: btns, Buttons: btns,
PointerID: pointer.ID(pointerID), PointerID: pointer.ID(pointerID),
@@ -1057,6 +1098,12 @@ func Java_org_gioui_GioView_imeSnippetStart(env *C.JNIEnv, class C.jclass, handl
//export Java_org_gioui_GioView_imeSetSnippet //export Java_org_gioui_GioView_imeSetSnippet
func Java_org_gioui_GioView_imeSetSnippet(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) { func Java_org_gioui_GioView_imeSetSnippet(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) {
w := cgo.Handle(handle).Value().(*window) w := cgo.Handle(handle).Value().(*window)
if start < 0 {
start = 0
}
if end < start {
end = start
}
r := key.Range{Start: int(start), End: int(end)} r := key.Range{Start: int(start), End: int(end)}
w.callbacks.SetEditorSnippet(r) w.callbacks.SetEditorSnippet(r)
} }
@@ -1132,22 +1179,41 @@ func (w *window) ShowTextInput(show bool) {
} }
func (w *window) SetInputHint(mode key.InputHint) { func (w *window) SetInputHint(mode key.InputHint) {
w.inputHint = mode
// Constants defined at https://developer.android.com/reference/android/text/InputType. // Constants defined at https://developer.android.com/reference/android/text/InputType.
const ( const (
TYPE_NULL = 0 TYPE_NULL = 0
TYPE_CLASS_TEXT = 1
TYPE_CLASS_NUMBER = 2 TYPE_CLASS_TEXT = 1
TYPE_NUMBER_FLAG_DECIMAL = 8192 TYPE_TEXT_VARIATION_EMAIL_ADDRESS = 32
TYPE_NUMBER_FLAG_SIGNED = 4096 TYPE_TEXT_VARIATION_URI = 16
TYPE_TEXT_FLAG_NO_SUGGESTIONS = 524288 TYPE_TEXT_VARIATION_PASSWORD = 128
TYPE_TEXT_VARIATION_VISIBLE_PASSWORD = 144 TYPE_TEXT_FLAG_CAP_SENTENCES = 16384
TYPE_TEXT_FLAG_AUTO_CORRECT = 32768
TYPE_CLASS_NUMBER = 2
TYPE_NUMBER_FLAG_DECIMAL = 8192
TYPE_NUMBER_FLAG_SIGNED = 4096
TYPE_CLASS_PHONE = 3
) )
runInJVM(javaVM(), func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
var m jvalue var m jvalue
switch mode { switch mode {
case key.HintText:
m = TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_AUTO_CORRECT | TYPE_TEXT_FLAG_CAP_SENTENCES
case key.HintNumeric: case key.HintNumeric:
m = TYPE_CLASS_NUMBER | TYPE_NUMBER_FLAG_DECIMAL | TYPE_NUMBER_FLAG_SIGNED m = TYPE_CLASS_NUMBER | TYPE_NUMBER_FLAG_DECIMAL | TYPE_NUMBER_FLAG_SIGNED
case key.HintEmail:
m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_EMAIL_ADDRESS
case key.HintURL:
m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_URI
case key.HintTelephone:
m = TYPE_CLASS_PHONE
case key.HintPassword:
m = TYPE_CLASS_TEXT | TYPE_TEXT_VARIATION_PASSWORD
default: default:
m = TYPE_CLASS_TEXT m = TYPE_CLASS_TEXT
} }
@@ -1259,16 +1325,17 @@ func findClass(env *C.JNIEnv, name string) C.jclass {
} }
func osMain() { func osMain() {
select {}
} }
func newWindow(window *callbacks, options []Option) error { func newWindow(window *callbacks, options []Option) {
mainWindow.in <- windowAndConfig{window, options} 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) { runInJVM(javaVM(), func(env *C.JNIEnv) {
jstr := javaString(env, s) jstr := javaString(env, string(s))
callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard, callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
jvalue(android.appCtx), jvalue(jstr)) jvalue(android.appCtx), jvalue(jstr))
}) })
@@ -1282,52 +1349,57 @@ func (w *window) ReadClipboard() {
return return
} }
content := goString(env, C.jstring(c)) 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) { func (w *window) Configure(options []Option) {
cnf := w.config
cnf.apply(unit.Metric{}, options)
runInJVM(javaVM(), func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
prev := w.config w.setConfig(env, cnf)
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
}
if w.config != prev {
w.callbacks.Event(ConfigEvent{Config: w.config})
}
}) })
} }
func (w *window) Perform(system.Action) {} func (w *window) setConfig(env *C.JNIEnv, cnf Config) {
prev := w.config
// Decorations are never disabled.
cnf.Decorated = true
func (w *window) Raise() {} 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) { func (w *window) SetCursor(cursor pointer.Cursor) {
runInJVM(javaVM(), func(env *C.JNIEnv) { runInJVM(javaVM(), func(env *C.JNIEnv) {
@@ -1335,9 +1407,10 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
}) })
} }
func (w *window) Wakeup() { func (w *window) wakeup() {
runOnMain(func(env *C.JNIEnv) { runOnMain(func(env *C.JNIEnv) {
w.callbacks.Event(wakeupEvent{}) w.loop.Wakeup()
w.loop.FlushEvents()
}) })
} }
@@ -1406,9 +1479,6 @@ func setNavigationColor(env *C.JNIEnv, view C.jobject, color color.NRGBA) {
) )
} }
// Close the window. Not implemented for Android.
func (w *window) Close() {}
// runOnMain runs a function on the Java main thread. // runOnMain runs a function on the Java main thread.
func runOnMain(f func(env *C.JNIEnv)) { func runOnMain(f func(env *C.JNIEnv)) {
go func() { go func() {
@@ -1431,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{})
}
+35 -30
View File
@@ -5,8 +5,8 @@ package app
/* /*
#include <Foundation/Foundation.h> #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(uintptr_t handle); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void);
__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl); __attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl);
__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl); __attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl); __attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
@@ -40,9 +40,11 @@ static CFTypeRef newNSString(unichar *chars, NSUInteger length) {
} }
*/ */
import "C" import "C"
import ( import (
"errors" "errors"
"runtime/cgo" "runtime/cgo"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
"unicode/utf16" "unicode/utf16"
@@ -70,30 +72,28 @@ type displayLink struct {
running uint32 running uint32
} }
var mainFuncs = make(chan func(), 1) // displayLinks maps CFTypeRefs to *displayLinks.
var displayLinks sync.Map
func isMainThread() bool {
return bool(C.isMainThread())
}
// runOnMain runs the function on the main thread. // runOnMain runs the function on the main thread.
func runOnMain(f func()) { func runOnMain(f func()) {
if C.isMainThread() { if isMainThread() {
f() f()
return return
} }
go func() { C.gio_runOnMain(C.uintptr_t(cgo.NewHandle(f)))
mainFuncs <- f
C.gio_wakeupMainThread()
}()
} }
//export gio_dispatchMainFuncs //export gio_runFunc
func gio_dispatchMainFuncs() { func gio_runFunc(h C.uintptr_t) {
for { handle := cgo.Handle(h)
select { defer handle.Delete()
case f := <-mainFuncs: f := handle.Value().(func())
f() f()
default:
return
}
}
} }
// nsstringToString converts a NSString to a Go string. // nsstringToString converts a NSString to a Go string.
@@ -121,25 +121,25 @@ func stringToNSString(str string) C.CFTypeRef {
return C.newNSString(chars, C.NSUInteger(len(u16))) return C.newNSString(chars, C.NSUInteger(len(u16)))
} }
func NewDisplayLink(callback func()) (*displayLink, error) { func newDisplayLink(callback func()) (*displayLink, error) {
d := &displayLink{ d := &displayLink{
callback: callback, callback: callback,
done: make(chan struct{}), done: make(chan struct{}),
states: make(chan bool), states: make(chan bool),
dids: make(chan uint64), dids: make(chan uint64),
} }
h := cgo.NewHandle(d) dl := C.gio_createDisplayLink()
dl := C.gio_createDisplayLink(C.uintptr_t(h))
if dl == 0 { if dl == 0 {
return nil, errors.New("app: failed to create display link") return nil, errors.New("app: failed to create display link")
} }
go d.run(dl, h) go d.run(dl)
return d, nil return d, nil
} }
func (d *displayLink) run(dl C.CFTypeRef, h cgo.Handle) { func (d *displayLink) run(dl C.CFTypeRef) {
defer C.gio_releaseDisplayLink(dl) defer C.gio_releaseDisplayLink(dl)
defer h.Delete() displayLinks.Store(dl, d)
defer displayLinks.Delete(dl)
var stopTimer *time.Timer var stopTimer *time.Timer
var tchan <-chan time.Time var tchan <-chan time.Time
started := false started := false
@@ -200,10 +200,14 @@ func (d *displayLink) SetDisplayID(did uint64) {
} }
//export gio_onFrameCallback //export gio_onFrameCallback
func gio_onFrameCallback(dl C.CFTypeRef, handle C.uintptr_t) { func gio_onFrameCallback(ref C.CFTypeRef) {
d := cgo.Handle(handle).Value().(*displayLink) d, exists := displayLinks.Load(ref)
if atomic.LoadUint32(&d.running) != 0 { if !exists {
d.callback() return
}
dl := d.(*displayLink)
if atomic.LoadUint32(&dl.running) != 0 {
dl.callback()
} }
} }
@@ -253,8 +257,9 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
return to return to
} }
func (w *window) Wakeup() { func (w *window) wakeup() {
runOnMain(func() { runOnMain(func() {
w.w.Event(wakeupEvent{}) w.loop.Wakeup()
w.loop.FlushEvents()
}) })
} }
+3 -4
View File
@@ -1,12 +1,11 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
@import Dispatch; #import <Foundation/Foundation.h>
@import Foundation;
#include "_cgo_export.h" #include "_cgo_export.h"
void gio_wakeupMainThread(void) { void gio_runOnMain(uintptr_t h) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
gio_dispatchMainFuncs(); gio_runFunc(h);
}); });
} }
+181 -90
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build darwin && ios //go:build darwin && ios
// +build darwin,ios
package app package app
@@ -12,6 +11,9 @@ package app
#include <UIKit/UIKit.h> #include <UIKit/UIKit.h>
#include <stdint.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 { struct drawParams {
CGFloat dpi, sdpi; CGFloat dpi, sdpi;
CGFloat width, height; CGFloat width, height;
@@ -19,6 +21,7 @@ struct drawParams {
}; };
static void writeClipboard(unichar *chars, NSUInteger length) { static void writeClipboard(unichar *chars, NSUInteger length) {
#if !TARGET_OS_TV
@autoreleasepool { @autoreleasepool {
NSString *s = [NSString string]; NSString *s = [NSString string];
if (length > 0) { if (length > 0) {
@@ -27,13 +30,18 @@ static void writeClipboard(unichar *chars, NSUInteger length) {
UIPasteboard *p = UIPasteboard.generalPasteboard; UIPasteboard *p = UIPasteboard.generalPasteboard;
p.string = s; p.string = s;
} }
#endif
} }
static CFTypeRef readClipboard(void) { static CFTypeRef readClipboard(void) {
#if !TARGET_OS_TV
@autoreleasepool { @autoreleasepool {
UIPasteboard *p = UIPasteboard.generalPasteboard; UIPasteboard *p = UIPasteboard.generalPasteboard;
return (__bridge_retained CFTypeRef)p.string; return (__bridge_retained CFTypeRef)p.string;
} }
#else
return nil;
#endif
} }
static void showTextInput(CFTypeRef viewRef) { static void showTextInput(CFTypeRef viewRef) {
@@ -72,21 +80,27 @@ import "C"
import ( import (
"image" "image"
"io"
"os"
"runtime" "runtime"
"runtime/cgo"
"runtime/debug" "runtime/debug"
"strings"
"time" "time"
"unicode/utf16" "unicode/utf16"
"unsafe" "unsafe"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard" "gioui.org/io/event"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit" "gioui.org/unit"
) )
type ViewEvent struct { type UIKitViewEvent struct {
// ViewController is a CFTypeRef for the UIViewController backing a Window. // ViewController is a CFTypeRef for the UIViewController backing a Window.
ViewController uintptr ViewController uintptr
} }
@@ -95,18 +109,17 @@ type window struct {
view C.CFTypeRef view C.CFTypeRef
w *callbacks w *callbacks
displayLink *displayLink displayLink *displayLink
loop *eventLoop
visible bool hidden bool
cursor pointer.Cursor cursor pointer.Cursor
config Config config Config
pointerMap []C.CFTypeRef pointerMap []C.CFTypeRef
} }
var mainWindow = newWindowRendezvous() var mainWindow = newWindowRendezvous()
var views = make(map[C.CFTypeRef]*window)
func init() { func init() {
// Darwin requires UI operations happen on the main thread only. // Darwin requires UI operations happen on the main thread only.
runtime.LockOSThread() runtime.LockOSThread()
@@ -114,85 +127,99 @@ func init() {
//export onCreate //export onCreate
func onCreate(view, controller C.CFTypeRef) { func onCreate(view, controller C.CFTypeRef) {
wopts := <-mainWindow.out
w := &window{ w := &window{
view: view, 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) w.draw(false)
}) })
if err != nil { if err != nil {
panic(err) w.w.ProcessEvent(DestroyEvent{Err: err})
return
} }
w.displayLink = dl w.displayLink = dl
wopts := <-mainWindow.out C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
w.w = wopts.window
w.w.SetDriver(w)
views[view] = w
w.Configure(wopts.options) w.Configure(wopts.options)
w.w.Event(system.StageEvent{Stage: system.StagePaused}) w.ProcessEvent(UIKitViewEvent{ViewController: uintptr(controller)})
w.w.Event(ViewEvent{ViewController: uintptr(controller)}) }
func viewFor(h C.uintptr_t) *window {
return cgo.Handle(h).Value().(*window)
} }
//export gio_onDraw //export gio_onDraw
func gio_onDraw(view C.CFTypeRef) { func gio_onDraw(h C.uintptr_t) {
w := views[view] w := viewFor(h)
w.draw(true) w.draw(true)
} }
func (w *window) draw(sync bool) { func (w *window) draw(sync bool) {
if w.hidden {
return
}
params := C.viewDrawParams(w.view) params := C.viewDrawParams(w.view)
if params.width == 0 || params.height == 0 { if params.width == 0 || params.height == 0 {
return return
} }
wasVisible := w.visible
w.visible = true
if !wasVisible {
w.w.Event(system.StageEvent{Stage: system.StageRunning})
}
const inchPrDp = 1.0 / 163 const inchPrDp = 1.0 / 163
w.w.Event(frameEvent{ m := unit.Metric{
FrameEvent: system.FrameEvent{ PxPerDp: float32(params.dpi) * inchPrDp,
PxPerSp: float32(params.sdpi) * inchPrDp,
}
dppp := unit.Dp(1. / m.PxPerDp)
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: image.Point{ Size: image.Point{
X: int(params.width + .5), X: int(params.width + .5),
Y: int(params.height + .5), Y: int(params.height + .5),
}, },
Insets: system.Insets{ Insets: Insets{
Top: unit.Px(float32(params.top)), Top: unit.Dp(params.top) * dppp,
Bottom: unit.Px(float32(params.bottom)), Bottom: unit.Dp(params.bottom) * dppp,
Left: unit.Px(float32(params.left)), Left: unit.Dp(params.left) * dppp,
Right: unit.Px(float32(params.right)), Right: unit.Dp(params.right) * dppp,
},
Metric: unit.Metric{
PxPerDp: float32(params.dpi) * inchPrDp,
PxPerSp: float32(params.sdpi) * inchPrDp,
}, },
Metric: m,
}, },
Sync: sync, Sync: sync,
}) })
} }
//export onStop //export onStop
func onStop(view C.CFTypeRef) { func onStop(h C.uintptr_t) {
w := views[view] w := viewFor(h)
w.visible = false w.hidden = true
w.w.Event(system.StageEvent{Stage: system.StagePaused}) }
//export onStart
func onStart(h C.uintptr_t) {
w := viewFor(h)
w.hidden = false
w.draw(true)
} }
//export onDestroy //export onDestroy
func onDestroy(view C.CFTypeRef) { func onDestroy(h C.uintptr_t) {
w := views[view] w := viewFor(h)
delete(views, view) w.ProcessEvent(UIKitViewEvent{})
w.w.Event(ViewEvent{}) w.ProcessEvent(DestroyEvent{})
w.w.Event(system.DestroyEvent{})
w.displayLink.Close() w.displayLink.Close()
w.displayLink = nil
cgo.Handle(h).Delete()
w.view = 0 w.view = 0
} }
//export onFocus //export onFocus
func onFocus(view C.CFTypeRef, focus int) { func onFocus(h C.uintptr_t, focus int) {
w := views[view] w := viewFor(h)
w.w.Event(key.FocusEvent{Focus: focus != 0}) w.config.Focused = focus != 0
w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export onLowMemory //export onLowMemory
@@ -202,56 +229,56 @@ func onLowMemory() {
} }
//export onUpArrow //export onUpArrow
func onUpArrow(view C.CFTypeRef) { func onUpArrow(h C.uintptr_t) {
views[view].onKeyCommand(key.NameUpArrow) viewFor(h).onKeyCommand(key.NameUpArrow)
} }
//export onDownArrow //export onDownArrow
func onDownArrow(view C.CFTypeRef) { func onDownArrow(h C.uintptr_t) {
views[view].onKeyCommand(key.NameDownArrow) viewFor(h).onKeyCommand(key.NameDownArrow)
} }
//export onLeftArrow //export onLeftArrow
func onLeftArrow(view C.CFTypeRef) { func onLeftArrow(h C.uintptr_t) {
views[view].onKeyCommand(key.NameLeftArrow) viewFor(h).onKeyCommand(key.NameLeftArrow)
} }
//export onRightArrow //export onRightArrow
func onRightArrow(view C.CFTypeRef) { func onRightArrow(h C.uintptr_t) {
views[view].onKeyCommand(key.NameRightArrow) viewFor(h).onKeyCommand(key.NameRightArrow)
} }
//export onDeleteBackward //export onDeleteBackward
func onDeleteBackward(view C.CFTypeRef) { func onDeleteBackward(h C.uintptr_t) {
views[view].onKeyCommand(key.NameDeleteBackward) viewFor(h).onKeyCommand(key.NameDeleteBackward)
} }
//export onText //export onText
func onText(view, str C.CFTypeRef) { func onText(h C.uintptr_t, str C.CFTypeRef) {
w := views[view] w := viewFor(h)
w.w.EditorInsert(nsstringToString(str)) w.w.EditorInsert(nsstringToString(str))
} }
//export onTouch //export onTouch
func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) { func onTouch(h C.uintptr_t, last C.int, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
var typ pointer.Type var kind pointer.Kind
switch phase { switch phase {
case C.UITouchPhaseBegan: case C.UITouchPhaseBegan:
typ = pointer.Press kind = pointer.Press
case C.UITouchPhaseMoved: case C.UITouchPhaseMoved:
typ = pointer.Move kind = pointer.Move
case C.UITouchPhaseEnded: case C.UITouchPhaseEnded:
typ = pointer.Release kind = pointer.Release
case C.UITouchPhaseCancelled: case C.UITouchPhaseCancelled:
typ = pointer.Cancel kind = pointer.Cancel
default: default:
return return
} }
w := views[view] w := viewFor(h)
t := time.Duration(float64(ti) * float64(time.Second)) t := time.Duration(float64(ti) * float64(time.Second))
p := f32.Point{X: float32(x), Y: float32(y)} p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: typ, Kind: kind,
Source: pointer.Touch, Source: pointer.Touch,
PointerID: w.lookupTouch(last != 0, touchRef), PointerID: w.lookupTouch(last != 0, touchRef),
Position: p, Position: p,
@@ -263,11 +290,16 @@ func (w *window) ReadClipboard() {
cstr := C.readClipboard() cstr := C.readClipboard()
defer C.CFRelease(cstr) defer C.CFRelease(cstr)
content := nsstringToString(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) { func (w *window) WriteClipboard(mime string, s []byte) {
u16 := utf16.Encode([]rune(s)) u16 := utf16.Encode([]rune(string(s)))
var chars *C.unichar var chars *C.unichar
if len(u16) > 0 { if len(u16) > 0 {
chars = (*C.unichar)(unsafe.Pointer(&u16[0])) chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
@@ -276,25 +308,16 @@ func (w *window) WriteClipboard(s string) {
} }
func (w *window) Configure([]Option) { func (w *window) Configure([]Option) {
prev := w.config
// Decorations are never disabled. // Decorations are never disabled.
w.config.Decorated = true w.config.Decorated = true
if w.config != prev { w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
} }
func (w *window) EditorStateChanged(old, new editorState) {} func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) Perform(system.Action) {} func (w *window) Perform(system.Action) {}
func (w *window) Raise() {}
func (w *window) SetAnimating(anim bool) { func (w *window) SetAnimating(anim bool) {
v := w.view
if v == 0 {
return
}
if anim { if anim {
w.displayLink.Start() w.displayLink.Start()
} else { } else {
@@ -306,8 +329,8 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
w.cursor = windowSetCursor(w.cursor, cursor) w.cursor = windowSetCursor(w.cursor, cursor)
} }
func (w *window) onKeyCommand(name string) { func (w *window) onKeyCommand(name key.Name) {
w.w.Event(key.Event{ w.ProcessEvent(key.Event{
Name: name, Name: name,
}) })
} }
@@ -346,20 +369,88 @@ func (w *window) ShowTextInput(show bool) {
func (w *window) SetInputHint(_ key.InputHint) {} func (w *window) SetInputHint(_ key.InputHint) {}
// Close the window. Not implemented for iOS. func (w *window) ProcessEvent(e event.Event) {
func (w *window) Close() {} w.w.ProcessEvent(e)
w.loop.FlushEvents()
func newWindow(win *callbacks, options []Option) error {
mainWindow.in <- windowAndConfig{win, options}
return <-mainWindow.errs
} }
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() { 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 //export gio_runMain
func 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{})
}
+107 -50
View File
@@ -11,6 +11,7 @@
__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void); __attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
@interface GioView: UIView <UIKeyInput> @interface GioView: UIView <UIKeyInput>
@property uintptr_t handle;
@end @end
@implementation GioViewController @implementation GioViewController
@@ -25,12 +26,13 @@ CGFloat _keyboardHeight;
self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0); self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame]; UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame];
[self.view addSubview: drawView]; [self.view addSubview: drawView];
#ifndef TARGET_OS_TV #if !TARGET_OS_TV
drawView.multipleTouchEnabled = YES; drawView.multipleTouchEnabled = YES;
#endif #endif
drawView.preservesSuperviewLayoutMargins = YES; drawView.preservesSuperviewLayoutMargins = YES;
drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0); drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
onCreate((__bridge CFTypeRef)drawView, (__bridge CFTypeRef)self); onCreate((__bridge CFTypeRef)drawView, (__bridge CFTypeRef)self);
#if !TARGET_OS_TV
[[NSNotificationCenter defaultCenter] addObserver:self [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChange:) selector:@selector(keyboardWillChange:)
name:UIKeyboardWillShowNotification name:UIKeyboardWillShowNotification
@@ -43,6 +45,7 @@ CGFloat _keyboardHeight;
selector:@selector(keyboardWillHide:) selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification name:UIKeyboardWillHideNotification
object:nil]; object:nil];
#endif
[[NSNotificationCenter defaultCenter] addObserver: self [[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(applicationDidEnterBackground:) selector: @selector(applicationDidEnterBackground:)
name: UIApplicationDidEnterBackgroundNotification name: UIApplicationDidEnterBackgroundNotification
@@ -54,33 +57,33 @@ CGFloat _keyboardHeight;
} }
- (void)applicationWillEnterForeground:(UIApplication *)application { - (void)applicationWillEnterForeground:(UIApplication *)application {
UIView *drawView = self.view.subviews[0]; GioView *view = (GioView *)self.view.subviews[0];
if (drawView != nil) { if (view != nil) {
gio_onDraw((__bridge CFTypeRef)drawView); onStart(view.handle);
} }
} }
- (void)applicationDidEnterBackground:(UIApplication *)application { - (void)applicationDidEnterBackground:(UIApplication *)application {
UIView *drawView = self.view.subviews[0]; GioView *view = (GioView *)self.view.subviews[0];
if (drawView != nil) { if (view != nil) {
onStop((__bridge CFTypeRef)drawView); onStop(view.handle);
} }
} }
- (void)viewDidDisappear:(BOOL)animated { - (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated]; [super viewDidDisappear:animated];
CFTypeRef viewRef = (__bridge CFTypeRef)self.view.subviews[0]; GioView *view = (GioView *)self.view.subviews[0];
onDestroy(viewRef); onDestroy(view.handle);
} }
- (void)viewDidLayoutSubviews { - (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews]; [super viewDidLayoutSubviews];
UIView *view = self.view.subviews[0]; GioView *view = (GioView *)self.view.subviews[0];
CGRect frame = self.view.bounds; CGRect frame = self.view.bounds;
// Adjust view bounds to make room for the keyboard. // Adjust view bounds to make room for the keyboard.
frame.size.height -= _keyboardHeight; frame.size.height -= _keyboardHeight;
view.frame = frame; view.frame = frame;
gio_onDraw((__bridge CFTypeRef)view); gio_onDraw(view.handle);
} }
- (void)didReceiveMemoryWarning { - (void)didReceiveMemoryWarning {
@@ -88,6 +91,7 @@ CGFloat _keyboardHeight;
[super didReceiveMemoryWarning]; [super didReceiveMemoryWarning];
} }
#if !TARGET_OS_TV
- (void)keyboardWillChange:(NSNotification *)note { - (void)keyboardWillChange:(NSNotification *)note {
NSDictionary *userInfo = note.userInfo; NSDictionary *userInfo = note.userInfo;
CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
@@ -99,13 +103,13 @@ CGFloat _keyboardHeight;
_keyboardHeight = 0.0; _keyboardHeight = 0.0;
[self.view setNeedsLayout]; [self.view setNeedsLayout];
} }
#endif
@end @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; CGFloat scale = view.contentScaleFactor;
NSUInteger i = 0; NSUInteger i = 0;
NSUInteger n = [touches count]; NSUInteger n = [touches count];
CFTypeRef viewRef = (__bridge CFTypeRef)view;
for (UITouch *touch in touches) { for (UITouch *touch in touches) {
CFTypeRef touchRef = (__bridge CFTypeRef)touch; CFTypeRef touchRef = (__bridge CFTypeRef)touch;
i++; i++;
@@ -116,45 +120,84 @@ static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIE
CGPoint loc = [coalescedTouch locationInView:view]; CGPoint loc = [coalescedTouch locationInView:view];
j++; j++;
int lastTouch = last && i == n && j == m; 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]);
} }
} }
} }
@implementation GioView @implementation GioView
NSArray<UIKeyCommand *> *_keyCommands; NSArray<UIKeyCommand *> *_keyCommands;
+ (void)onFrameCallback:(CADisplayLink *)link {
gio_onFrameCallback((__bridge CFTypeRef)link);
}
+ (Class)layerClass { + (Class)layerClass {
return gio_layerClass(); return gio_layerClass();
} }
- (void)willMoveToWindow:(UIWindow *)newWindow { - (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) { if (self.window != nil) {
[[NSNotificationCenter defaultCenter] removeObserver:self [[NSNotificationCenter defaultCenter] removeObserver:self
name:UIWindowDidBecomeKeyNotification name:UIWindowDidBecomeKeyNotification
object:self.window]; object:self.window];
[[NSNotificationCenter defaultCenter] removeObserver:self [[NSNotificationCenter defaultCenter] removeObserver:self
name:UIWindowDidResignKeyNotification name:UIWindowDidResignKeyNotification
object:self.window]; object:self.window];
} }
self.contentScaleFactor = newWindow.screen.nativeScale;
[[NSNotificationCenter defaultCenter] addObserver:self [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onWindowDidBecomeKey:) selector:@selector(onWindowDidBecomeKey:)
name:UIWindowDidBecomeKeyNotification name:UIWindowDidBecomeKeyNotification
object:newWindow]; object:newWindow];
[[NSNotificationCenter defaultCenter] addObserver:self [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onWindowDidResignKey:) selector:@selector(onWindowDidResignKey:)
name:UIWindowDidResignKeyNotification name:UIWindowDidResignKeyNotification
object:newWindow]; object:newWindow];
} }
- (void)onWindowDidBecomeKey:(NSNotification *)note { - (void)onWindowDidBecomeKey:(NSNotification *)note {
if (self.isFirstResponder) { if (self.isFirstResponder) {
onFocus((__bridge CFTypeRef)self, YES); onFocus(self.handle, YES);
} }
} }
- (void)onWindowDidResignKey:(NSNotification *)note { - (void)onWindowDidResignKey:(NSNotification *)note {
if (self.isFirstResponder) { if (self.isFirstResponder) {
onFocus((__bridge CFTypeRef)self, NO); onFocus(self.handle, NO);
} }
} }
@@ -175,7 +218,7 @@ NSArray<UIKeyCommand *> *_keyCommands;
} }
- (void)insertText:(NSString *)text { - (void)insertText:(NSString *)text {
onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)text); onText(self.handle, (__bridge CFTypeRef)text);
} }
- (BOOL)canBecomeFirstResponder { - (BOOL)canBecomeFirstResponder {
@@ -187,23 +230,23 @@ NSArray<UIKeyCommand *> *_keyCommands;
} }
- (void)deleteBackward { - (void)deleteBackward {
onDeleteBackward((__bridge CFTypeRef)self); onDeleteBackward(self.handle);
} }
- (void)onUpArrow { - (void)onUpArrow {
onUpArrow((__bridge CFTypeRef)self); onUpArrow(self.handle);
} }
- (void)onDownArrow { - (void)onDownArrow {
onDownArrow((__bridge CFTypeRef)self); onDownArrow(self.handle);
} }
- (void)onLeftArrow { - (void)onLeftArrow {
onLeftArrow((__bridge CFTypeRef)self); onLeftArrow(self.handle);
} }
- (void)onRightArrow { - (void)onRightArrow {
onRightArrow((__bridge CFTypeRef)self); onRightArrow(self.handle);
} }
- (NSArray<UIKeyCommand *> *)keyCommands { - (NSArray<UIKeyCommand *> *)keyCommands {
@@ -227,23 +270,8 @@ NSArray<UIKeyCommand *> *_keyCommands;
} }
@end @end
@interface DisplayLinkHandle : NSObject { CFTypeRef gio_createDisplayLink(void) {
} CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)];
@property uintptr_t handle;
@end
@implementation DisplayLinkHandle {
}
- (void)onFrameCallback:(CADisplayLink *)link {
gio_onFrameCallback((__bridge CFTypeRef)link, _handle);
}
@end
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
DisplayLinkHandle *h = [DisplayLinkHandle alloc];
h.handle = handle;
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:h selector:@selector(onFrameCallback:)];
dl.paused = YES; dl.paused = YES;
NSRunLoop *runLoop = [NSRunLoop mainRunLoop]; NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
[dl addToRunLoop:runLoop forMode:[runLoop currentMode]]; [dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
@@ -283,3 +311,32 @@ void gio_showCursor() {
void gio_setCursor(NSUInteger curID) { void gio_setCursor(NSUInteger curID) {
// Not supported. // 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]));
}
}
+492 -118
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"io"
"strings" "strings"
"syscall/js" "syscall/js"
"time" "time"
@@ -13,16 +14,28 @@ import (
"unicode/utf8" "unicode/utf8"
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard" "gioui.org/io/event"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
type ViewEvent struct{} type JSViewEvent struct {
Element js.Value
}
type contextStatus int
const (
contextStatusOkay contextStatus = iota
contextStatusLost
contextStatusRestored
)
type window struct { type window struct {
window js.Value window js.Value
@@ -40,12 +53,10 @@ type window struct {
screenOrientation js.Value screenOrientation js.Value
cleanfuncs []func() cleanfuncs []func()
touches []js.Value touches []js.Value
composing bool composing int
lastCursor int
requestFocus bool requestFocus bool
chanAnimation chan struct{}
chanRedraw chan struct{}
config Config config Config
inset f32.Point inset f32.Point
scale float32 scale float32
@@ -54,9 +65,11 @@ type window struct {
// is pending. // is pending.
animRequested bool animRequested bool
wakeups chan struct{} wakeups chan struct{}
contextStatus contextStatus
} }
func newWindow(win *callbacks, options []Option) error { func newWindow(win *callbacks, options []Option) {
doc := js.Global().Get("document") doc := js.Global().Get("document")
cont := getContainer(doc) cont := getContainer(doc)
cnv := createCanvas(doc) cnv := createCanvas(doc)
@@ -71,7 +84,10 @@ func newWindow(win *callbacks, options []Option) error {
head: doc.Get("head"), head: doc.Get("head"),
clipboard: js.Global().Get("navigator").Get("clipboard"), clipboard: js.Global().Get("navigator").Get("clipboard"),
wakeups: make(chan struct{}, 1), wakeups: make(chan struct{}, 1),
w: win,
composing: -1,
} }
w.w.SetDriver(w)
w.requestAnimationFrame = w.window.Get("requestAnimationFrame") w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
w.browserHistory = w.window.Get("history") w.browserHistory = w.window.Get("history")
w.visualViewport = w.window.Get("visualViewport") w.visualViewport = w.window.Get("visualViewport")
@@ -81,41 +97,28 @@ func newWindow(win *callbacks, options []Option) error {
if screen := w.window.Get("screen"); screen.Truthy() { if screen := w.window.Get("screen"); screen.Truthy() {
w.screenOrientation = screen.Get("orientation") 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.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
w.chanAnimation <- struct{}{} w.draw(false)
return nil return nil
}) })
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} { w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
content := args[0].String() 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 return nil
}) })
w.addEventListeners() w.addEventListeners()
w.addHistory() w.addHistory()
w.w = win
go func() { w.Configure(options)
defer w.cleanup() w.blur()
w.w.SetDriver(w) w.processEvent(JSViewEvent{Element: cont})
w.Configure(options) w.resize()
w.blur() w.draw(true)
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
} }
func getContainer(doc js.Value) js.Value { func getContainer(doc js.Value) js.Value {
@@ -129,8 +132,10 @@ func getContainer(doc js.Value) js.Value {
} }
func createTextArea(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") style := tarea.Get("style")
// Position absolute so left/top coordinates actually place the element
style.Set("position", "absolute")
style.Set("width", "1px") style.Set("width", "1px")
style.Set("height", "1px") style.Set("height", "1px")
style.Set("opacity", "0") style.Set("opacity", "0")
@@ -140,6 +145,12 @@ func createTextArea(doc js.Value) js.Value {
tarea.Set("autocorrect", "off") tarea.Set("autocorrect", "off")
tarea.Set("autocapitalize", "off") tarea.Set("autocapitalize", "off")
tarea.Set("spellcheck", false) 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 return tarea
} }
@@ -162,9 +173,25 @@ func (w *window) cleanup() {
} }
func (w *window) addEventListeners() { func (w *window) addEventListeners() {
w.addEventListener(w.cnv, "webglcontextlost", func(this js.Value, args []js.Value) interface{} {
args[0].Call("preventDefault")
w.contextStatus = contextStatusLost
return nil
})
w.addEventListener(w.cnv, "webglcontextrestored", func(this js.Value, args []js.Value) interface{} {
args[0].Call("preventDefault")
w.contextStatus = contextStatusRestored
// Resize is required to force update the canvas content when restored.
w.cnv.Set("width", 0)
w.cnv.Set("height", 0)
w.resize()
w.draw(true)
return nil
})
w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
w.resize() w.resize()
w.chanRedraw <- struct{}{} w.draw(true)
return nil return nil
}) })
w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
@@ -172,25 +199,11 @@ func (w *window) addEventListeners() {
return nil return nil
}) })
w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
ev := &system.CommandEvent{Type: system.CommandBack} if w.processEvent(key.Event{Name: key.NameBack}) {
w.w.Event(ev)
if ev.Cancel {
return w.browserHistory.Call("forward") return w.browserHistory.Call("forward")
} }
return w.browserHistory.Call("back") 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.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
w.pointerEvent(pointer.Move, 0, 0, args[0]) w.pointerEvent(pointer.Move, 0, 0, args[0])
return nil return nil
@@ -210,6 +223,10 @@ func (w *window) addEventListeners() {
w.addEventListener(w.cnv, "wheel", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.cnv, "wheel", func(this js.Value, args []js.Value) interface{} {
e := args[0] e := args[0]
dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float() dx, dy := e.Get("deltaX").Float(), e.Get("deltaY").Float()
// horizontal scroll if shift is pressed.
if e.Get("shiftKey").Bool() {
dx, dy = dy, dx
}
mode := e.Get("deltaMode").Int() mode := e.Get("deltaMode").Int()
switch mode { switch mode {
case 0x01: // DOM_DELTA_LINE case 0x01: // DOM_DELTA_LINE
@@ -244,18 +261,28 @@ func (w *window) addEventListeners() {
w.touches[i] = js.Null() w.touches[i] = js.Null()
} }
w.touches = w.touches[:0] w.touches = w.touches[:0]
w.w.Event(pointer.Event{ w.processEvent(pointer.Event{
Type: pointer.Cancel, Kind: pointer.Cancel,
Source: pointer.Touch, Source: pointer.Touch,
}) })
return nil return nil
}) })
w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} { 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 return nil
}) })
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} { 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() w.blur()
return nil return nil
}) })
@@ -268,19 +295,205 @@ func (w *window) addEventListeners() {
return nil return nil
}) })
w.addEventListener(w.tarea, "compositionstart", func(this js.Value, args []js.Value) interface{} { 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 return nil
}) })
w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.tarea, "compositionend", func(this js.Value, args []js.Value) interface{} {
w.composing = false finalText := w.tarea.Get("value").String()
w.flushInput()
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 return nil
}) })
w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.tarea, "input", func(this js.Value, args []js.Value) interface{} {
if w.composing { e := args[0]
return nil 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 return nil
}) })
w.addEventListener(w.tarea, "paste", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.tarea, "paste", func(this js.Value, args []js.Value) interface{} {
@@ -297,12 +510,6 @@ func (w *window) addHistory() {
w.browserHistory.Call("pushState", nil, nil, w.window.Get("location").Get("href")) 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() { func (w *window) blur() {
w.tarea.Call("blur") w.tarea.Call("blur")
w.requestFocus = false w.requestFocus = false
@@ -328,24 +535,104 @@ func (w *window) keyboard(hint key.InputHint) {
m = "url" m = "url"
case key.HintTelephone: case key.HintTelephone:
m = "tel" m = "tel"
case key.HintPassword:
m = "password"
default: default:
m = "text" m = "text"
} }
w.tarea.Set("inputMode", m) 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) { func (w *window) keyEvent(e js.Value, ks key.State) {
k := e.Get("key").String() k := e.Get("key").String()
if n, ok := translateKey(k); ok { 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{ cmd := key.Event{
Name: n, Name: n,
Modifiers: modifiersFor(e), Modifiers: modifiersFor(e),
State: ks, 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 // modifiersFor returns the modifier set for a DOM MouseEvent or
// KeyEvent. // KeyEvent.
func modifiersFor(e js.Value) key.Modifiers { func modifiersFor(e js.Value) key.Modifiers {
@@ -363,10 +650,16 @@ func modifiersFor(e js.Value) key.Modifiers {
if e.Call("getModifierState", "Shift").Bool() { if e.Call("getModifierState", "Shift").Bool() {
mods |= key.ModShift mods |= key.ModShift
} }
if e.Call("getModifierState", "Meta").Bool() {
mods |= key.ModCommand
}
if e.Call("getModifierState", "OS").Bool() {
mods |= key.ModSuper
}
return mods 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") e.Call("preventDefault")
t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
changedTouches := e.Get("changedTouches") changedTouches := e.Get("changedTouches")
@@ -383,6 +676,9 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) {
if e.Get("ctrlKey").Bool() { if e.Get("ctrlKey").Bool() {
mods |= key.ModCtrl mods |= key.ModCtrl
} }
if e.Get("metaKey").Bool() {
mods |= key.ModCommand
}
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
touch := changedTouches.Index(i) touch := changedTouches.Index(i)
pid := w.touchIDFor(touch) pid := w.touchIDFor(touch)
@@ -393,8 +689,8 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) {
X: float32(x) * scale, X: float32(x) * scale,
Y: float32(y) * scale, Y: float32(y) * scale,
} }
w.w.Event(pointer.Event{ w.processEvent(pointer.Event{
Type: typ, Kind: kind,
Source: pointer.Touch, Source: pointer.Touch,
Position: pos, Position: pos,
PointerID: pid, PointerID: pid,
@@ -416,7 +712,7 @@ func (w *window) touchIDFor(touch js.Value) pointer.ID {
return pid 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") e.Call("preventDefault")
x, y := e.Get("clientX").Float(), e.Get("clientY").Float() x, y := e.Get("clientX").Float(), e.Get("clientY").Float()
rect := w.cnv.Call("getBoundingClientRect") rect := w.cnv.Call("getBoundingClientRect")
@@ -443,8 +739,8 @@ func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
if jbtns&4 != 0 { if jbtns&4 != 0 {
btns |= pointer.ButtonTertiary btns |= pointer.ButtonTertiary
} }
w.w.Event(pointer.Event{ w.processEvent(pointer.Event{
Type: typ, Kind: kind,
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: btns, Buttons: btns,
Position: pos, Position: pos,
@@ -470,19 +766,91 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
return jsf return jsf
} }
func (w *window) animCallback() { func (w *window) EditorStateChanged(old, new editorState) {
anim := w.animating if w.composing != -1 {
w.animRequested = anim // Do not interfere with browser state while composing.
if anim { // On Javascript is not possible to cancel the composition once started!
w.requestAnimationFrame.Invoke(w.redraw) 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) { func (w *window) SetAnimating(anim bool) {
w.animating = anim w.animating = anim
if anim && !w.animRequested { if anim && !w.animRequested {
@@ -495,20 +863,19 @@ func (w *window) ReadClipboard() {
if w.clipboard.IsUndefined() { if w.clipboard.IsUndefined() {
return return
} }
if w.clipboard.Get("readText").IsUndefined() { if w.clipboard.Get("readText").Truthy() {
return 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() { if w.clipboard.IsUndefined() {
return return
} }
if w.clipboard.Get("writeText").IsUndefined() { if w.clipboard.Get("writeText").IsUndefined() {
return return
} }
w.clipboard.Call("writeText", s) w.clipboard.Call("writeText", string(s))
} }
func (w *window) Configure(options []Option) { func (w *window) Configure(options []Option) {
@@ -536,15 +903,11 @@ func (w *window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated { if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated w.config.Decorated = cnf.Decorated
} }
if w.config != prev { w.processEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
} }
func (w *window) Perform(system.Action) {} func (w *window) Perform(system.Action) {}
func (w *window) Raise() {}
var webCursor = [...]string{ var webCursor = [...]string{
pointer.CursorDefault: "default", pointer.CursorDefault: "default",
pointer.CursorNone: "none", pointer.CursorNone: "none",
@@ -579,32 +942,27 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
style.Set("cursor", webCursor[cursor]) style.Set("cursor", webCursor[cursor])
} }
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
}
func (w *window) ShowTextInput(show bool) { func (w *window) ShowTextInput(show bool) {
// Run in a goroutine to avoid a deadlock if the // Run in a goroutine to avoid a deadlock if the
// focus change result in an event. // focus change result in an event.
go func() { if show {
if show { w.focus()
w.focus() } else {
} else { // If we're composing, end composition first by clearing the textarea.
w.blur() // 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) { func (w *window) SetInputHint(mode key.InputHint) {
w.keyboard(mode) w.keyboard(mode)
} }
// Close the window. Not implemented for js.
func (w *window) Close() {}
func (w *window) resize() { func (w *window) resize() {
w.scale = float32(w.window.Get("devicePixelRatio").Float()) w.scale = float32(w.window.Get("devicePixelRatio").Float())
@@ -615,7 +973,7 @@ func (w *window) resize() {
} }
if size != w.config.Size { if size != w.config.Size {
w.config.Size = 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() { if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
@@ -632,12 +990,23 @@ func (w *window) resize() {
} }
func (w *window) draw(sync bool) { 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() size, insets, metric := w.getConfig()
if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 { if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
return return
} }
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{ w.processEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: size, Size: size,
Insets: insets, Insets: insets,
@@ -647,11 +1016,12 @@ 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), return image.Pt(w.config.Size.X, w.config.Size.Y),
system.Insets{ Insets{
Bottom: unit.Px(w.inset.Y), Bottom: unit.Dp(w.inset.Y) * invscale,
Right: unit.Px(w.inset.X), Right: unit.Dp(w.inset.X) * invscale,
}, unit.Metric{ }, unit.Metric{
PxPerDp: w.scale, PxPerDp: w.scale,
PxPerSp: w.scale, PxPerSp: w.scale,
@@ -709,8 +1079,8 @@ func osMain() {
select {} select {}
} }
func translateKey(k string) (string, bool) { func translateKey(k string) (key.Name, bool) {
var n string var n key.Name
switch k { switch k {
case "ArrowUp": case "ArrowUp":
@@ -777,11 +1147,15 @@ func translateKey(k string) (string, bool) {
r, s := utf8.DecodeRuneInString(k) r, s := utf8.DecodeRuneInString(k)
// If there is exactly one printable character, return that. // If there is exactly one printable character, return that.
if s == len(k) && unicode.IsPrint(r) { if s == len(k) && unicode.IsPrint(r) {
return strings.ToUpper(k), true return key.Name(strings.ToUpper(k)), true
} }
return "", false return "", false
} }
return n, true return n, true
} }
func (_ ViewEvent) ImplementsEvent() {} func (JSViewEvent) implementsViewEvent() {}
func (JSViewEvent) ImplementsEvent() {}
func (j JSViewEvent) Valid() bool {
return !(j.Element.IsNull() || j.Element.IsUndefined())
}
+614 -335
View File
File diff suppressed because it is too large Load Diff
+142 -79
View File
@@ -2,11 +2,11 @@
// +build darwin,!ios // +build darwin,!ios
@import AppKit; #import <AppKit/AppKit.h>
#include "_cgo_export.h" #include "_cgo_export.h"
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void); __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWithTrans);
@interface GioAppDelegate : NSObject<NSApplicationDelegate> @interface GioAppDelegate : NSObject<NSApplicationDelegate>
@end @end
@@ -14,45 +14,55 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioWindowDelegate : NSObject<NSWindowDelegate> @interface GioWindowDelegate : NSObject<NSWindowDelegate>
@end @end
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@property uintptr_t handle;
@property BOOL presentWithTrans;
@end
@implementation GioWindowDelegate @implementation GioWindowDelegate
- (void)windowWillMiniaturize:(NSNotification *)notification { - (void)windowWillMiniaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
gio_onHide((__bridge CFTypeRef)window.contentView); GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
} }
- (void)windowDidDeminiaturize:(NSNotification *)notification { - (void)windowDidDeminiaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
gio_onShow((__bridge CFTypeRef)window.contentView); GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
} }
- (void)windowWillEnterFullScreen:(NSNotification *)notification { - (void)windowWillEnterFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
gio_onFullscreen((__bridge CFTypeRef)window.contentView); GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
} }
- (void)windowWillExitFullScreen:(NSNotification *)notification { - (void)windowWillExitFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
gio_onWindowed((__bridge CFTypeRef)window.contentView); GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
} }
- (void)windowDidChangeScreen:(NSNotification *)notification { - (void)windowDidChangeScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue]; CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
CFTypeRef view = (__bridge CFTypeRef)window.contentView; GioView *view = (GioView *)window.contentView;
gio_onChangeScreen(view, dispID); gio_onChangeScreen(view.handle, dispID);
} }
- (void)windowDidBecomeKey:(NSNotification *)notification { - (void)windowDidBecomeKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; 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 { - (void)windowDidResignKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
gio_onFocus((__bridge CFTypeRef)window.contentView, 0); GioView *view = (GioView *)window.contentView;
} if ([window firstResponder] == view) {
- (void)windowWillClose:(NSNotification *)notification { gio_onFocus(view.handle, 0);
NSWindow *window = (NSWindow *)[notification object]; }
window.delegate = nil;
gio_onClose((__bridge CFTypeRef)window.contentView);
} }
@end @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]; NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
if (!event.hasPreciseScrollingDeltas) { if (!event.hasPreciseScrollingDeltas) {
// dx and dy are in rows and columns. // dx and dy are in rows and columns.
@@ -61,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. // Origin is in the lower left corner. Convert to upper left.
CGFloat height = view.bounds.size.height; CGFloat height = view.bounds.size.height;
gio_onMouse((__bridge CFTypeRef)view, typ, [NSEvent pressedMouseButtons], 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 @implementation GioView
- (void)setFrameSize:(NSSize)newSize { - (void)setFrameSize:(NSSize)newSize {
[super setFrameSize:newSize]; [super setFrameSize:newSize];
@@ -75,75 +82,87 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
// drawRect is called when OpenGL is used, displayLayer otherwise. // drawRect is called when OpenGL is used, displayLayer otherwise.
// Don't know why. // Don't know why.
- (void)drawRect:(NSRect)r { - (void)drawRect:(NSRect)r {
gio_onDraw((__bridge CFTypeRef)self); gio_onDraw(self.handle);
} }
- (void)displayLayer:(CALayer *)layer { - (void)displayLayer:(CALayer *)layer {
layer.contentsScale = self.window.backingScaleFactor; layer.contentsScale = self.window.backingScaleFactor;
gio_onDraw((__bridge CFTypeRef)self); gio_onDraw(self.handle);
} }
- (CALayer *)makeBackingLayer { - (CALayer *)makeBackingLayer {
CALayer *layer = gio_layerFactory(); CALayer *layer = gio_layerFactory(self.presentWithTrans);
layer.delegate = self; layer.delegate = self;
return layer; return layer;
} }
- (void)viewDidMoveToWindow {
gio_onAttached(self.handle, self.window != nil ? 1 : 0);
}
- (void)mouseDown:(NSEvent *)event { - (void)mouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0); handleMouse(self, event, MOUSE_DOWN, 0, 0);
} }
- (void)mouseUp:(NSEvent *)event { - (void)mouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0); handleMouse(self, event, MOUSE_UP, 0, 0);
} }
- (void)middleMouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
}
- (void)middletMouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0);
}
- (void)rightMouseDown:(NSEvent *)event { - (void)rightMouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0); handleMouse(self, event, MOUSE_DOWN, 0, 0);
} }
- (void)rightMouseUp:(NSEvent *)event { - (void)rightMouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0); handleMouse(self, event, MOUSE_UP, 0, 0);
} }
- (void)otherMouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
}
- (void)otherMouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0);
}
- (void)mouseMoved:(NSEvent *)event { - (void)mouseMoved:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0); handleMouse(self, event, MOUSE_MOVE, 0, 0);
} }
- (void)mouseDragged:(NSEvent *)event { - (void)mouseDragged:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0); handleMouse(self, event, MOUSE_MOVE, 0, 0);
} }
- (void)rightMouseDragged:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
- (void)otherMouseDragged:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
- (void)scrollWheel:(NSEvent *)event { - (void)scrollWheel:(NSEvent *)event {
CGFloat dx = -event.scrollingDeltaX; CGFloat dx = -event.scrollingDeltaX;
CGFloat dy = -event.scrollingDeltaY; CGFloat dy = -event.scrollingDeltaY;
handleMouse(self, event, MOUSE_SCROLL, dx, dy); handleMouse(self, event, MOUSE_SCROLL, dx, dy);
} }
- (void)keyDown:(NSEvent *)event { - (void)keyDown:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
NSString *keys = [event charactersIgnoringModifiers]; 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 { - (void)keyUp:(NSEvent *)event {
NSString *keys = [event charactersIgnoringModifiers]; 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 { - (void)insertText:(id)string {
gio_onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)string); gio_onText(self.handle, (__bridge CFTypeRef)string);
} }
- (void)doCommandBySelector:(SEL)sel { - (void)doCommandBySelector:(SEL)action {
// Don't pass commands up the responder chain. if (!gio_onCommandBySelector(self.handle)) {
// They will end up in a beep. [super doCommandBySelector:action];
}
} }
- (BOOL)hasMarkedText { - (BOOL)hasMarkedText {
int res = gio_hasMarkedText((__bridge CFTypeRef)self); int res = gio_hasMarkedText(self.handle);
return res ? YES : NO; return res ? YES : NO;
} }
- (NSRange)markedRange { - (NSRange)markedRange {
return gio_markedRange((__bridge CFTypeRef)self); return gio_markedRange(self.handle);
} }
- (NSRange)selectedRange { - (NSRange)selectedRange {
return gio_selectedRange((__bridge CFTypeRef)self); return gio_selectedRange(self.handle);
} }
- (void)unmarkText { - (void)unmarkText {
gio_unmarkText((__bridge CFTypeRef)self); gio_unmarkText(self.handle);
} }
- (void)setMarkedText:(id)string - (void)setMarkedText:(id)string
selectedRange:(NSRange)selRange selectedRange:(NSRange)selRange
@@ -155,14 +174,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
} else { } else {
str = string; str = string;
} }
gio_setMarkedText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, selRange, replaceRange); gio_setMarkedText(self.handle, (__bridge CFTypeRef)str, selRange, replaceRange);
} }
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText { - (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
return nil; return nil;
} }
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
actualRange:(NSRangePointer)actualRange { 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]; return [[NSAttributedString alloc] initWithString:str attributes:nil];
} }
- (void)insertText:(id)string - (void)insertText:(id)string
@@ -174,17 +193,34 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
} else { } else {
str = string; str = string;
} }
gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange); gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange);
} }
- (NSUInteger)characterIndexForPoint:(NSPoint)p { - (NSUInteger)characterIndexForPoint:(NSPoint)p {
return gio_characterIndexForPoint((__bridge CFTypeRef)self, p); return gio_characterIndexForPoint(self.handle, p);
} }
- (NSRect)firstRectForCharacterRange:(NSRange)rng - (NSRect)firstRectForCharacterRange:(NSRange)rng
actualRange:(NSRangePointer)actual { 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]; r = [self convertRect:r toView:nil];
return [[self window] convertRectToScreen:r]; 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 @end
// Delegates are weakly referenced from their peers. Nothing // Delegates are weakly referenced from their peers. Nothing
@@ -193,14 +229,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
static GioWindowDelegate *globalWindowDel; static GioWindowDelegate *globalWindowDel;
static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) { static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) {
gio_onFrameCallback(dl, (uintptr_t)handle); gio_onFrameCallback(dl);
return kCVReturnSuccess; return kCVReturnSuccess;
} }
CFTypeRef gio_createDisplayLink(uintptr_t handle) { CFTypeRef gio_createDisplayLink(void) {
CVDisplayLinkRef dl; CVDisplayLinkRef dl;
CVDisplayLinkCreateWithActiveCGDisplays(&dl); CVDisplayLinkCreateWithActiveCGDisplays(&dl);
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, (void *)(handle)); CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil);
return dl; return dl;
} }
@@ -234,7 +270,7 @@ void gio_showCursor() {
// some cursors are not public, this tries to use a private cursor // some cursors are not public, this tries to use a private cursor
// and uses fallback when the use of private cursor fails. // 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]) { if ([NSCursor respondsToSelector:cursorName]) {
id object = [NSCursor performSelector:cursorName]; id object = [NSCursor performSelector:cursorName];
if ([object isKindOfClass:[NSCursor class]]) { if ([object isKindOfClass:[NSCursor class]]) {
@@ -266,7 +302,7 @@ void gio_setCursor(NSUInteger curID) {
break; break;
case 6: // pointer.CursorAllScroll case 6: // pointer.CursorAllScroll
// For some reason, using _moveCursor fails on Monterey. // For some reason, using _moveCursor fails on Monterey.
// gio_trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor); // trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
[NSCursor.arrowCursor set]; [NSCursor.arrowCursor set];
break; break;
case 7: // pointer.CursorColResize case 7: // pointer.CursorColResize
@@ -276,33 +312,31 @@ void gio_setCursor(NSUInteger curID) {
[NSCursor.resizeUpDownCursor set]; [NSCursor.resizeUpDownCursor set];
break; break;
case 9: // pointer.CursorGrab case 9: // pointer.CursorGrab
// [NSCursor.openHandCursor set]; [NSCursor.openHandCursor set];
gio_trySetPrivateCursor(@selector(openHandCursor), NSCursor.arrowCursor);
break; break;
case 10: // pointer.CursorGrabbing case 10: // pointer.CursorGrabbing
// [NSCursor.closedHandCursor set]; [NSCursor.closedHandCursor set];
gio_trySetPrivateCursor(@selector(closedHandCursor), NSCursor.arrowCursor);
break; break;
case 11: // pointer.CursorNotAllowed case 11: // pointer.CursorNotAllowed
[NSCursor.operationNotAllowedCursor set]; [NSCursor.operationNotAllowedCursor set];
break; break;
case 12: // pointer.CursorWait case 12: // pointer.CursorWait
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor); trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break; break;
case 13: // pointer.CursorProgress case 13: // pointer.CursorProgress
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor); trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break; break;
case 14: // pointer.CursorNorthWestResize case 14: // pointer.CursorNorthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor); trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
break; break;
case 15: // pointer.CursorNorthEastResize case 15: // pointer.CursorNorthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor); trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
break; break;
case 16: // pointer.CursorSouthWestResize case 16: // pointer.CursorSouthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor); trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
break; break;
case 17: // pointer.CursorSouthEastResize case 17: // pointer.CursorSouthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor); trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
break; break;
case 18: // pointer.CursorNorthSouthResize case 18: // pointer.CursorNorthSouthResize
[NSCursor.resizeUpDownCursor set]; [NSCursor.resizeUpDownCursor set];
@@ -323,10 +357,10 @@ void gio_setCursor(NSUInteger curID) {
[NSCursor.resizeDownCursor set]; [NSCursor.resizeDownCursor set];
break; break;
case 24: // pointer.CursorNorthEastSouthWestResize case 24: // pointer.CursorNorthEastSouthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor); trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
break; break;
case 25: // pointer.CursorNorthWestSouthEastResize case 25: // pointer.CursorNorthWestSouthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor); trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
break; break;
default: default:
[NSCursor.arrowCursor set]; [NSCursor.arrowCursor set];
@@ -335,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 { @autoreleasepool {
NSRect rect = NSMakeRect(0, 0, width, height); NSRect rect = NSMakeRect(0, 0, width, height);
NSUInteger styleMask = NSTitledWindowMask | NSUInteger styleMask = NSTitledWindowMask |
@@ -347,43 +381,50 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
styleMask:styleMask styleMask:styleMask
backing:NSBackingStoreBuffered backing:NSBackingStoreBuffered
defer:NO]; 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]; [window setAcceptsMouseMovedEvents:YES];
NSView *view = (__bridge NSView *)viewRef; NSView *view = (__bridge NSView *)viewRef;
[window setContentView:view]; [window setContentView:view];
[window makeFirstResponder:view];
window.releasedWhenClosed = NO;
window.delegate = globalWindowDel; window.delegate = globalWindowDel;
return (__bridge_retained CFTypeRef)window; return (__bridge_retained CFTypeRef)window;
} }
} }
CFTypeRef gio_createView(void) { CFTypeRef gio_createView(int presentWithTrans) {
@autoreleasepool { @autoreleasepool {
NSRect frame = NSMakeRect(0, 0, 0, 0); NSRect frame = NSMakeRect(0, 0, 0, 0);
GioView* view = [[GioView alloc] initWithFrame:frame]; GioView* view = [[GioView alloc] initWithFrame:frame];
view.presentWithTrans = presentWithTrans ? YES : NO;
view.wantsLayer = YES; view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; 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); return CFBridgingRetain(view);
} }
} }
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
@autoreleasepool {
GioView *v = (__bridge GioView *)viewRef;
v.handle = handle;
}
}
@implementation GioAppDelegate @implementation GioAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES]; [NSApp activateIgnoringOtherApps:YES];
gio_onFinishLaunching();
} }
- (void)applicationDidHide:(NSNotification *)aNotification { - (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
gio_onAppHide(); for (NSURL *url in urls) {
} gio_onOpenURI((__bridge CFTypeRef)url.absoluteString);
- (void)applicationWillUnhide:(NSNotification *)notification { }
gio_onAppShow();
} }
@end @end
@@ -414,3 +455,25 @@ void gio_main() {
[NSApp run]; [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 // SPDX-License-Identifier: Unlicense OR MIT
//go:build (linux && !android) || freebsd || openbsd //go:build (linux && !android) || freebsd || openbsd
// +build linux,!android freebsd openbsd
package app package app
@@ -12,13 +11,6 @@ import (
"gioui.org/io/pointer" "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 { type X11ViewEvent struct {
// Display is a pointer to the X11 Display created by XOpenDisplay. // Display is a pointer to the X11 Display created by XOpenDisplay.
Display unsafe.Pointer Display unsafe.Pointer
@@ -28,6 +20,9 @@ type X11ViewEvent struct {
func (X11ViewEvent) implementsViewEvent() {} func (X11ViewEvent) implementsViewEvent() {}
func (X11ViewEvent) ImplementsEvent() {} func (X11ViewEvent) ImplementsEvent() {}
func (x X11ViewEvent) Valid() bool {
return x != (X11ViewEvent{})
}
type WaylandViewEvent struct { type WaylandViewEvent struct {
// Display is the *wl_display returned by wl_display_connect. // Display is the *wl_display returned by wl_display_connect.
@@ -38,6 +33,9 @@ type WaylandViewEvent struct {
func (WaylandViewEvent) implementsViewEvent() {} func (WaylandViewEvent) implementsViewEvent() {}
func (WaylandViewEvent) ImplementsEvent() {} func (WaylandViewEvent) ImplementsEvent() {}
func (w WaylandViewEvent) Valid() bool {
return w != (WaylandViewEvent{})
}
func osMain() { func osMain() {
select {} select {}
@@ -49,7 +47,7 @@ type windowDriver func(*callbacks, []Option) error
// let each driver initialize these variables with their own version of createWindow. // let each driver initialize these variables with their own version of createWindow.
var wlDriver, x11Driver windowDriver var wlDriver, x11Driver windowDriver
func newWindow(window *callbacks, options []Option) error { func newWindow(window *callbacks, options []Option) {
var errFirst error var errFirst error
for _, d := range []windowDriver{wlDriver, x11Driver} { for _, d := range []windowDriver{wlDriver, x11Driver} {
if d == nil { if d == nil {
@@ -57,16 +55,16 @@ func newWindow(window *callbacks, options []Option) error {
} }
err := d(window, options) err := d(window, options)
if err == nil { if err == nil {
return nil return
} }
if errFirst == nil { if errFirst == nil {
errFirst = err errFirst = err
} }
} }
if errFirst != nil { if errFirst == nil {
return errFirst 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. // xCursor contains mapping from pointer.Cursor to XCursor.
+410 -222
View File
File diff suppressed because it is too large Load Diff
+677 -254
View File
File diff suppressed because it is too large Load Diff
+176 -131
View File
@@ -26,22 +26,25 @@ package app
*/ */
import "C" import "C"
import ( import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"os" "io"
"path/filepath"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
"unsafe" "unsafe"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/clipboard" "gioui.org/io/event"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit" "gioui.org/unit"
syscall "golang.org/x/sys/unix" syscall "golang.org/x/sys/unix"
@@ -93,12 +96,10 @@ type x11Window struct {
// _NET_WM_STATE_MAXIMIZED_VERT // _NET_WM_STATE_MAXIMIZED_VERT
wmStateMaximizedVert C.Atom wmStateMaximizedVert C.Atom
} }
stage system.Stage
metric unit.Metric metric unit.Metric
notify struct { notify struct {
read, write int read, write int
} }
dead bool
animating bool animating bool
@@ -111,6 +112,8 @@ type x11Window struct {
config Config config Config
wakeups chan struct{} wakeups chan struct{}
handler x11EventHandler
buf [100]byte
} }
var ( var (
@@ -153,8 +156,8 @@ func (w *x11Window) ReadClipboard() {
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime) C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
} }
func (w *x11Window) WriteClipboard(s string) { func (w *x11Window) WriteClipboard(mime string, s []byte) {
w.clipboard.content = []byte(s) w.clipboard.content = s
C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime) C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime) C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime)
} }
@@ -172,7 +175,7 @@ func (w *x11Window) Configure(options []Option) {
switch prev.Mode { switch prev.Mode {
case Fullscreen: case Fullscreen:
case Minimized: case Minimized:
w.Raise() w.raise()
fallthrough fallthrough
default: default:
w.config.Mode = Fullscreen w.config.Mode = Fullscreen
@@ -190,7 +193,7 @@ func (w *x11Window) Configure(options []Option) {
switch prev.Mode { switch prev.Mode {
case Fullscreen: case Fullscreen:
case Minimized: case Minimized:
w.Raise() w.raise()
fallthrough fallthrough
default: default:
w.config.Mode = Maximized w.config.Mode = Maximized
@@ -205,7 +208,7 @@ func (w *x11Window) Configure(options []Option) {
C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y)) C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y))
case Minimized: case Minimized:
w.config.Mode = Windowed w.config.Mode = Windowed
w.Raise() w.raise()
case Maximized: case Maximized:
w.config.Mode = Windowed w.config.Mode = Windowed
w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert) w.sendWMStateEvent(_NET_WM_STATE_REMOVE, w.atoms.wmStateMaximizedHorz, w.atoms.wmStateMaximizedVert)
@@ -230,29 +233,11 @@ func (w *x11Window) Configure(options []Option) {
if shints.flags != 0 { if shints.flags != 0 {
C.XSetWMNormalHints(w.x, w.xw, &shints) C.XSetWMNormalHints(w.x, w.xw, &shints)
} }
if cnf.center {
screen := C.XDefaultScreen(w.x)
width := C.XDisplayWidth(w.x, screen)
height := C.XDisplayHeight(w.x, screen)
var attrs C.XWindowAttributes
C.XGetWindowAttributes(w.x, w.xw, &attrs)
width -= attrs.border_width
height -= attrs.border_width
sz := w.config.Size
x := (int(width) - sz.X) / 2
y := (int(height) - sz.Y) / 2
C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y))
}
} }
if cnf.Decorated != prev.Decorated { if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated w.config.Decorated = cnf.Decorated
} }
if w.config != prev { w.ProcessEvent(ConfigEvent{Config: w.config})
w.w.Event(ConfigEvent{Config: w.config})
}
} }
func (w *x11Window) setTitle(prev, cnf Config) { func (w *x11Window) setTitle(prev, cnf Config) {
@@ -273,9 +258,38 @@ func (w *x11Window) setTitle(prev, cnf Config) {
} }
} }
func (w *x11Window) Perform(system.Action) {} func (w *x11Window) Perform(acts system.Action) {
walkActions(acts, func(a system.Action) {
switch a {
case system.ActionCenter:
w.center()
case system.ActionRaise:
w.raise()
}
})
if acts&system.ActionClose != 0 {
w.close()
}
}
func (w *x11Window) Raise() { func (w *x11Window) center() {
screen := C.XDefaultScreen(w.x)
width := C.XDisplayWidth(w.x, screen)
height := C.XDisplayHeight(w.x, screen)
var attrs C.XWindowAttributes
C.XGetWindowAttributes(w.x, w.xw, &attrs)
width -= attrs.border_width
height -= attrs.border_width
sz := w.config.Size
x := (int(width) - sz.X) / 2
y := (int(height) - sz.Y) / 2
C.XMoveResizeWindow(w.x, w.xw, C.int(x), C.int(y), C.uint(sz.X), C.uint(sz.Y))
}
func (w *x11Window) raise() {
var xev C.XEvent var xev C.XEvent
ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
*ev = C.XClientMessageEvent{ *ev = C.XClientMessageEvent{
@@ -321,8 +335,8 @@ func (w *x11Window) SetInputHint(_ key.InputHint) {}
func (w *x11Window) EditorStateChanged(old, new editorState) {} func (w *x11Window) EditorStateChanged(old, new editorState) {}
// Close the window. // close the window.
func (w *x11Window) Close() { func (w *x11Window) close() {
var xev C.XEvent var xev C.XEvent
ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev)) ev := (*C.XClientMessageEvent)(unsafe.Pointer(&xev))
*ev = C.XClientMessageEvent{ *ev = C.XClientMessageEvent{
@@ -366,7 +380,36 @@ func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) {
var x11OneByte = make([]byte, 1) 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 { select {
case w.wakeups <- struct{}{}: case w.wakeups <- struct{}{}:
default: default:
@@ -384,16 +427,20 @@ func (w *x11Window) window() (C.Window, int, int) {
return w.xw, w.config.Size.X, w.config.Size.Y return w.xw, w.config.Size.X, w.config.Size.Y
} }
func (w *x11Window) setStage(s system.Stage) { func (w *x11Window) dispatch() {
if s == w.stage { if w.x == nil {
// Only Invalidate can wake us up.
<-w.wakeups
w.w.Invalidate()
return return
} }
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
}
func (w *x11Window) loop() { select {
h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)} case <-w.wakeups:
w.w.Invalidate()
default:
}
xfd := C.XConnectionNumber(w.x) xfd := C.XConnectionNumber(w.x)
// Poll for events and notifications. // Poll for events and notifications.
@@ -403,62 +450,55 @@ func (w *x11Window) loop() {
} }
xEvents := &pollfds[0].Revents xEvents := &pollfds[0].Revents
// Plenty of room for a backlog of notifications. // Plenty of room for a backlog of notifications.
buf := make([]byte, 100)
loop: var syn, anim bool
for !w.dead { // Check for pending draw events before checking animation or blocking.
var syn, anim bool // This fixes an issue on Xephyr where on startup XPending() > 0 but
// Check for pending draw events before checking animation or blocking. // poll will still block. This also prevents no-op calls to poll.
// This fixes an issue on Xephyr where on startup XPending() > 0 but syn = w.handler.handleEvents()
// poll will still block. This also prevents no-op calls to poll. if w.x == nil {
if syn = h.handleEvents(); !syn { // handleEvents received a close request and destroyed the window.
anim = w.animating return
if !anim { }
// Clear poll events. if !syn {
*xEvents = 0 anim = w.animating
// Wait for X event or gio notification. if !anim {
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { // Clear poll events.
panic(fmt.Errorf("x11 loop: poll failed: %w", err)) *xEvents = 0
} // Wait for X event or gio notification.
switch { if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
case *xEvents&syscall.POLLIN != 0: panic(fmt.Errorf("x11 loop: poll failed: %w", err))
syn = h.handleEvents() }
if w.dead { switch {
break loop case *xEvents&syscall.POLLIN != 0:
} syn = w.handler.handleEvents()
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0: if w.x == nil {
break loop 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))
}
}
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,
})
}
} }
w.w.Event(system.DestroyEvent{Err: nil}) // Clear notifications.
for {
_, err := syscall.Read(w.notify.read, w.buf[:])
if err == syscall.EAGAIN {
break
}
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
}
}
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: w.metric,
},
Sync: syn,
})
}
} }
func (w *x11Window) destroy() { func (w *x11Window) destroy() {
@@ -476,11 +516,11 @@ func (w *x11Window) destroy() {
} }
C.XDestroyWindow(w.x, w.xw) C.XDestroyWindow(w.x, w.xw)
C.XCloseDisplay(w.x) C.XCloseDisplay(w.x)
w.x = nil
} }
// atom is a wrapper around XInternAtom. Callers should cache the result // atom is a wrapper around XInternAtom. Callers should cache the result
// in order to limit round-trips to the X server. // in order to limit round-trips to the X server.
//
func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom { func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom {
cname := C.CString(name) cname := C.CString(name)
defer C.free(unsafe.Pointer(cname)) defer C.free(unsafe.Pointer(cname))
@@ -494,7 +534,6 @@ func (w *x11Window) atom(name string, onlyIfExists bool) C.Atom {
// x11EventHandler wraps static variables for the main event loop. // x11EventHandler wraps static variables for the main event loop.
// Its sole purpose is to prevent heap allocation and reduce clutter // Its sole purpose is to prevent heap allocation and reduce clutter
// in x11window.loop. // in x11window.loop.
//
type x11EventHandler struct { type x11EventHandler struct {
w *x11Window w *x11Window
text []byte text []byte
@@ -502,7 +541,6 @@ type x11EventHandler struct {
} }
// handleEvents returns true if the window needs to be redrawn. // handleEvents returns true if the window needs to be redrawn.
//
func (h *x11EventHandler) handleEvents() bool { func (h *x11EventHandler) handleEvents() bool {
w := h.w w := h.w
xev := h.xev xev := h.xev
@@ -536,13 +574,13 @@ func (h *x11EventHandler) handleEvents() bool {
// There's no support for IME yet. // There's no support for IME yet.
w.w.EditorInsert(ee.Text) w.w.EditorInsert(ee.Text)
} else { } else {
w.w.Event(e) w.ProcessEvent(e)
} }
} }
case C.ButtonPress, C.ButtonRelease: case C.ButtonPress, C.ButtonRelease:
bevt := (*C.XButtonEvent)(unsafe.Pointer(xev)) bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
ev := pointer.Event{ ev := pointer.Event{
Type: pointer.Press, Kind: pointer.Press,
Source: pointer.Mouse, Source: pointer.Mouse,
Position: f32.Point{ Position: f32.Point{
X: float32(bevt.x), X: float32(bevt.x),
@@ -552,7 +590,7 @@ func (h *x11EventHandler) handleEvents() bool {
Modifiers: w.xkb.Modifiers(), Modifiers: w.xkb.Modifiers(),
} }
if bevt._type == C.ButtonRelease { if bevt._type == C.ButtonRelease {
ev.Type = pointer.Release ev.Kind = pointer.Release
} }
var btn pointer.Buttons var btn pointer.Buttons
const scrollScale = 10 const scrollScale = 10
@@ -564,21 +602,29 @@ func (h *x11EventHandler) handleEvents() bool {
case C.Button3: case C.Button3:
btn = pointer.ButtonSecondary btn = pointer.ButtonSecondary
case C.Button4: case C.Button4:
// scroll up ev.Kind = pointer.Scroll
ev.Type = pointer.Scroll // scroll up or left (if shift is pressed).
ev.Scroll.Y = -scrollScale if ev.Modifiers == key.ModShift {
ev.Scroll.X = -scrollScale
} else {
ev.Scroll.Y = -scrollScale
}
case C.Button5: case C.Button5:
// scroll down // scroll down or right (if shift is pressed).
ev.Type = pointer.Scroll ev.Kind = pointer.Scroll
ev.Scroll.Y = +scrollScale if ev.Modifiers == key.ModShift {
ev.Scroll.X = +scrollScale
} else {
ev.Scroll.Y = +scrollScale
}
case 6: case 6:
// http://xahlee.info/linux/linux_x11_mouse_button_number.html // http://xahlee.info/linux/linux_x11_mouse_button_number.html
// scroll left // scroll left.
ev.Type = pointer.Scroll ev.Kind = pointer.Scroll
ev.Scroll.X = -scrollScale * 2 ev.Scroll.X = -scrollScale * 2
case 7: case 7:
// scroll right // scroll right
ev.Type = pointer.Scroll ev.Kind = pointer.Scroll
ev.Scroll.X = +scrollScale * 2 ev.Scroll.X = +scrollScale * 2
default: default:
continue continue
@@ -590,11 +636,11 @@ func (h *x11EventHandler) handleEvents() bool {
w.pointerBtns &^= btn w.pointerBtns &^= btn
} }
ev.Buttons = w.pointerBtns ev.Buttons = w.pointerBtns
w.w.Event(ev) w.ProcessEvent(ev)
case C.MotionNotify: case C.MotionNotify:
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev)) mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: pointer.Move, Kind: pointer.Move,
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: w.pointerBtns, Buttons: w.pointerBtns,
Position: f32.Point{ Position: f32.Point{
@@ -608,14 +654,16 @@ func (h *x11EventHandler) handleEvents() bool {
// redraw only on the last expose event // redraw only on the last expose event
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0 redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
case C.FocusIn: case C.FocusIn:
w.w.Event(key.FocusEvent{Focus: true}) w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
case C.FocusOut: 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 case C.ConfigureNotify: // window configuration change
cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev)) cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size { if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
w.config.Size = sz 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 // redraw will be done by a later expose event
case C.SelectionNotify: case C.SelectionNotify:
@@ -637,7 +685,12 @@ func (h *x11EventHandler) handleEvents() bool {
break break
} }
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems)) 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: case C.SelectionRequest:
cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev)) cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None { if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
@@ -691,7 +744,7 @@ func (h *x11EventHandler) handleEvents() bool {
cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev)) cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
switch *(*C.long)(unsafe.Pointer(&cevt.data)) { switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
case C.long(w.atoms.evDelWindow): case C.long(w.atoms.evDelWindow):
w.dead = true w.shutdown(nil)
return false return false
} }
} }
@@ -699,9 +752,7 @@ func (h *x11EventHandler) handleEvents() bool {
return redraw return redraw
} }
var ( var x11Threads sync.Once
x11Threads sync.Once
)
func init() { func init() {
x11Driver = newX11Window x11Driver = newX11Window
@@ -773,8 +824,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
wakeups: make(chan struct{}, 1), wakeups: make(chan struct{}, 1),
config: Config{Size: cnf.Size}, 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.read = pipe[0]
w.notify.write = pipe[1] w.notify.write = pipe[1]
w.w.SetDriver(w)
if err := w.updateXkbKeymap(); err != nil { if err := w.updateXkbKeymap(); err != nil {
w.destroy() w.destroy()
@@ -786,7 +839,7 @@ func newX11Window(gioWin *callbacks, options []Option) error {
hints.flags = C.InputHint hints.flags = C.InputHint
C.XSetWMHints(dpy, win, &hints) C.XSetWMHints(dpy, win, &hints)
name := C.CString(filepath.Base(os.Args[0])) name := C.CString(ID)
defer C.free(unsafe.Pointer(name)) defer C.free(unsafe.Pointer(name))
wmhints := C.XClassHint{name, name} wmhints := C.XClassHint{name, name}
C.XSetClassHint(dpy, win, &wmhints) C.XSetClassHint(dpy, win, &wmhints)
@@ -810,18 +863,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
// extensions // extensions
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1) C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
go func() { // make the window visible on the screen
w.w.SetDriver(w) C.XMapWindow(dpy, win)
w.Configure(options)
// make the window visible on the screen w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
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.destroy()
}()
return nil return nil
} }
+6 -6
View File
@@ -4,15 +4,15 @@
Package bluetooth implements permissions to access Bluetooth and Bluetooth Package bluetooth implements permissions to access Bluetooth and Bluetooth
Low Energy hardware, including the ability to discover and pair devices. Low Energy hardware, including the ability to discover and pair devices.
Android # Android
The following entries will be added to AndroidManifest.xml: The following entries will be added to AndroidManifest.xml:
<uses-permission android:name="android.permission.BLUETOOTH"/> <uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-feature android:name="android.hardware.bluetooth" android:required="false"/> <uses-feature android:name="android.hardware.bluetooth" android:required="false"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/> <uses-feature android:name="android.hardware.bluetooth_le" android:required="false"/>
Note that ACCESS_FINE_LOCATION is required on Android before the Bluetooth Note that ACCESS_FINE_LOCATION is required on Android before the Bluetooth
device may be used. device may be used.
+3 -3
View File
@@ -3,12 +3,12 @@
/* /*
Package camera implements permissions to access camera hardware. Package camera implements permissions to access camera hardware.
Android # Android
The following entries will be added to AndroidManifest.xml: The following entries will be added to AndroidManifest.xml:
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/> <uses-feature android:name="android.hardware.camera" android:required="false"/>
CAMERA is a "dangerous" permission. See documentation for package CAMERA is a "dangerous" permission. See documentation for package
gioui.org/app/permission for more information. gioui.org/app/permission for more information.
+1 -1
View File
@@ -34,7 +34,7 @@ program's source code:
_ "net" _ "net"
) )
Android -- Dangerous Permissions # Android -- Dangerous Permissions
Certain permissions on Android are marked with a protection level of Certain permissions on Android are marked with a protection level of
"dangerous". This means that, in addition to including the relevant "dangerous". This means that, in addition to including the relevant
+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
+2 -3
View File
@@ -3,11 +3,10 @@
/* /*
Package networkstate implements permissions to access network connectivity information. Package networkstate implements permissions to access network connectivity information.
Android # Android
The following entries will be added to AndroidManifest.xml: The following entries will be added to AndroidManifest.xml:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
*/ */
package networkstate package networkstate
+3 -3
View File
@@ -4,12 +4,12 @@
Package storage implements read and write storage permissions Package storage implements read and write storage permissions
on mobile devices. on mobile devices.
Android # Android
The following entries will be added to AndroidManifest.xml: The following entries will be added to AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE are "dangerous" permissions. READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE are "dangerous" permissions.
See documentation for package gioui.org/app/permission for more information. See documentation for package gioui.org/app/permission for more information.
+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: Unlicense OR MIT
/*
Package wakelock implements permission to acquire locks that keep the system
from suspending.
# Android
The following entries will be added to AndroidManifest.xml:
<uses-permission android:name="android.permission.WAKE_LOCK"/>
*/
package wakelock
+1 -2
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build android || (darwin && ios) //go:build android || (darwin && ios)
// +build android darwin,ios
package app package app
@@ -25,6 +24,6 @@ func runMain() {
// Indirect call, since the linker does not know the address of main when // Indirect call, since the linker does not know the address of main when
// laying down this package. // laying down this package.
fn := mainMain 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 { if err != nil {
return err 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 { if width < minExt.X || maxExt.X < width || height < minExt.Y || maxExt.Y < height {
return errOutOfDate return errOutOfDate
} }
-1
View File
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
//go:build !novulkan //go:build !novulkan
// +build !novulkan
package app package app
+515 -622
View File
File diff suppressed because it is too large Load Diff
-14
View File
@@ -1,14 +0,0 @@
module gioui.org/cmd
go 1.13
require (
gioui.org v0.0.0-20220328154813-a3f147541fd0
github.com/akavel/rsrc v0.10.1
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4
github.com/chromedp/chromedp v0.5.2
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/text v0.3.6
golang.org/x/tools v0.1.0
)
-475
View File
@@ -1,475 +0,0 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
gioui.org v0.0.0-20220328154813-a3f147541fd0 h1:n4FUiCT6P4a2wF6hwX4a5R8TpjAhu/d+3nhwZW16MAI=
gioui.org v0.0.0-20220328154813-a3f147541fd0/go.mod h1:b8vBukexG6eYuXZa14asjLAWJ+JjbZ/ophEnS2FjYUg=
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.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/akavel/rsrc v0.10.1 h1:hCCPImjmFKVNGpeLZyTDRHEFC283DzyTXTo0cO0Rq9o=
github.com/akavel/rsrc v0.10.1/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
github.com/benoitkugler/textlayout v0.0.5/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8=
github.com/benoitkugler/textlayout v0.0.10 h1:uIaQgH4pBFw1LQ0tPkfjgxo94WYcckzzQaB41L2X84w=
github.com/benoitkugler/textlayout v0.0.10/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4 h1:QD3KxSJ59L2lxG6MXBjNHxiQO2RmxTQ3XcK+wO44WOg=
github.com/chromedp/cdproto v0.0.0-20191114225735-6626966fbae4/go.mod h1:PfAWWKJqjlGFYJEidUM6aVIWPr0EpobeyVWEEmplX7g=
github.com/chromedp/chromedp v0.5.2 h1:W8xBXQuUnd2dZK0SN/lyVwsQM7KgW+kY5HGnntms194=
github.com/chromedp/chromedp v0.5.2/go.mod h1:rsTo/xRo23KZZwFmWk2Ui79rBaVRRATCjLzNQlOFSiA=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12 h1:1bjaB/5IIicfKpP4k0s30T2WEw//Kh00zULa8DQ0cxA=
github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12/go.mod h1:kDhBRTA/i3H46PVdhqcw26TdGSIj42TOKNWKY+Kipnw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506 h1:1TPz/Gn/MsXwJ6bEtI9wdkPcQYr2X3V9I+wz4wPYUdY=
github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506/go.mod h1:R0mlTNeyszZ/tKQhbZA7SRGjx+OHsmNzgN2jTV7yZcs=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8=
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08 h1:V0an7KRw92wmJysvFvtqtKMAPmvS5O0jtB0nYo6t+gs=
github.com/knq/sysutil v0.0.0-20191005231841-15668db23d08/go.mod h1:dFWs1zEqDjFtnBXsd1vPOZaLsESovai349994nHx3e0=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3 h1:IlrJD2AM5p8JhN/wVny9jt6gJ9hut2VALhSeZ3SYluk=
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191113165036-4c7a9d0fe056/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
-143
View File
@@ -1,143 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main_test
import (
"bytes"
"context"
"fmt"
"image"
"image/png"
"os"
"os/exec"
"path/filepath"
"regexp"
)
type AndroidTestDriver struct {
driverBase
sdkDir string
adbPath string
}
var rxAdbDevice = regexp.MustCompile(`(.*)\s+device$`)
func (d *AndroidTestDriver) Start(path string) {
d.sdkDir = os.Getenv("ANDROID_SDK_ROOT")
if d.sdkDir == "" {
d.Skipf("Android SDK is required; set $ANDROID_SDK_ROOT")
}
d.adbPath = filepath.Join(d.sdkDir, "platform-tools", "adb")
if _, err := os.Stat(d.adbPath); os.IsNotExist(err) {
d.Skipf("adb not found")
}
devOut := bytes.TrimSpace(d.adb("devices"))
devices := rxAdbDevice.FindAllSubmatch(devOut, -1)
switch len(devices) {
case 0:
d.Skipf("no Android devices attached via adb; skipping")
case 1:
default:
d.Skipf("multiple Android devices attached via adb; skipping")
}
// If the device is attached but asleep, it's probably just charging.
// Don't use it; the screen needs to be on and unlocked for the test to
// work.
if !bytes.Contains(
d.adb("shell", "dumpsys", "power"),
[]byte(" mWakefulness=Awake"),
) {
d.Skipf("Android device isn't awake; skipping")
}
// First, build the app.
apk := filepath.Join(d.tempDir("gio-endtoend-android"), "e2e.apk")
d.gogio("-target=android", "-appid="+appid, "-o="+apk, path)
// Make sure the app isn't installed already, and try to uninstall it
// when we finish. Previous failed test runs might have left the app.
d.tryUninstall()
d.adb("install", apk)
d.Cleanup(d.tryUninstall)
// Force our e2e app to be fullscreen, so that the android system bar at
// the top doesn't mess with our screenshots.
// TODO(mvdan): is there a way to do this via gio, so that we don't need
// to set up a global Android setting via the shell?
d.adb("shell", "settings", "put", "global", "policy_control", "immersive.full="+appid)
// Make sure the app isn't already running.
d.adb("shell", "pm", "clear", appid)
// Start listening for log messages.
{
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, d.adbPath,
"logcat",
"-s", // suppress other logs
"-T1", // don't show previous log messages
appid+":*", // show all logs from our gio app ID
)
output, err := cmd.StdoutPipe()
if err != nil {
d.Fatal(err)
}
cmd.Stderr = cmd.Stdout
d.output = output
if err := cmd.Start(); err != nil {
d.Fatal(err)
}
d.Cleanup(cancel)
}
// Start the app.
d.adb("shell", "monkey", "-p", appid, "1")
// Wait for the gio app to render.
d.waitForFrame()
}
func (d *AndroidTestDriver) Screenshot() image.Image {
out := d.adb("shell", "screencap", "-p")
img, err := png.Decode(bytes.NewReader(out))
if err != nil {
d.Fatal(err)
}
return img
}
func (d *AndroidTestDriver) tryUninstall() {
cmd := exec.Command(d.adbPath, "shell", "pm", "uninstall", appid)
out, err := cmd.CombinedOutput()
if err != nil {
if bytes.Contains(out, []byte("Unknown package")) {
// The package is not installed. Don't log anything.
return
}
d.Logf("could not uninstall: %v\n%s", err, out)
}
}
func (d *AndroidTestDriver) adb(args ...interface{}) []byte {
strs := []string{}
for _, arg := range args {
strs = append(strs, fmt.Sprint(arg))
}
cmd := exec.Command(d.adbPath, strs...)
out, err := cmd.CombinedOutput()
if err != nil {
d.Errorf("%s", out)
d.Fatal(err)
}
return out
}
func (d *AndroidTestDriver) Click(x, y int) {
d.adb("shell", "input", "tap", x, y)
// Wait for the gio app to render after this click.
d.waitForFrame()
}
File diff suppressed because it is too large Load Diff
-156
View File
@@ -1,156 +0,0 @@
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
)
type buildInfo struct {
appID string
archs []string
ldflags string
minsdk int
name string
pkgDir string
pkgPath string
iconPath string
tags string
target string
version int
key string
password string
}
func newBuildInfo(pkgPath string) (*buildInfo, error) {
pkgMetadata, err := getPkgMetadata(pkgPath)
if err != nil {
return nil, err
}
appID := getAppID(pkgMetadata)
appIcon := filepath.Join(pkgMetadata.Dir, "appicon.png")
if *iconPath != "" {
appIcon = *iconPath
}
bi := &buildInfo{
appID: appID,
archs: getArchs(),
ldflags: getLdFlags(appID),
minsdk: *minsdk,
name: getPkgName(pkgMetadata),
pkgDir: pkgMetadata.Dir,
pkgPath: pkgPath,
iconPath: appIcon,
tags: *extraTags,
target: *target,
version: *version,
key: *signKey,
password: *signPass,
}
return bi, nil
}
func getArchs() []string {
if *archNames != "" {
return strings.Split(*archNames, ",")
}
switch *target {
case "js":
return []string{"wasm"}
case "ios", "tvos":
// Only 64-bit support.
return []string{"arm64", "amd64"}
case "android":
return []string{"arm", "arm64", "386", "amd64"}
case "windows":
goarch := os.Getenv("GOARCH")
if goarch == "" {
goarch = runtime.GOARCH
}
return []string{goarch}
default:
// TODO: Add flag tests.
panic("The target value has already been validated, this will never execute.")
}
}
func getLdFlags(appID string) string {
var ldflags []string
if extra := *extraLdflags; extra != "" {
ldflags = append(ldflags, strings.Split(extra, " ")...)
}
// Pass appID along, to be used for logging on platforms like Android.
ldflags = append(ldflags, fmt.Sprintf("-X gioui.org/app/internal/log.appID=%s", appID))
// Pass along all remaining arguments to the app.
if appArgs := flag.Args()[1:]; len(appArgs) > 0 {
ldflags = append(ldflags, fmt.Sprintf("-X gioui.org/app.extraArgs=%s", strings.Join(appArgs, "|")))
}
if m := *linkMode; m != "" {
ldflags = append(ldflags, "-linkmode="+m)
}
return strings.Join(ldflags, " ")
}
type packageMetadata struct {
PkgPath string
Dir string
}
func getPkgMetadata(pkgPath string) (*packageMetadata, error) {
pkgImportPath, err := runCmd(exec.Command("go", "list", "-f", "{{.ImportPath}}", pkgPath))
if err != nil {
return nil, err
}
pkgDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", pkgPath))
if err != nil {
return nil, err
}
return &packageMetadata{
PkgPath: pkgImportPath,
Dir: pkgDir,
}, nil
}
func getAppID(pkgMetadata *packageMetadata) string {
if *appID != "" {
return *appID
}
elems := strings.Split(pkgMetadata.PkgPath, "/")
domain := strings.Split(elems[0], ".")
name := ""
if len(elems) > 1 {
name = "." + elems[len(elems)-1]
}
if len(elems) < 2 && len(domain) < 2 {
name = "." + domain[0]
domain[0] = "localhost"
} else {
for i := 0; i < len(domain)/2; i++ {
opp := len(domain) - 1 - i
domain[i], domain[opp] = domain[opp], domain[i]
}
}
pkgDomain := strings.Join(domain, ".")
appid := []rune(pkgDomain + name)
// a Java-language-style package name may contain upper- and lower-case
// letters and underscores with individual parts separated by '.'.
// https://developer.android.com/guide/topics/manifest/manifest-element
for i, c := range appid {
if !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' ||
c == '_' || c == '.') {
appid[i] = '_'
}
}
return string(appid)
}
func getPkgName(pkgMetadata *packageMetadata) string {
return path.Base(pkgMetadata.PkgPath)
}
-32
View File
@@ -1,32 +0,0 @@
package main
import "testing"
type expval struct {
in, out string
}
func TestAppID(t *testing.T) {
t.Parallel()
tests := []expval{
{"example", "localhost.example"},
{"example.com", "com.example"},
{"www.example.com", "com.example.www"},
{"examplecom/app", "examplecom.app"},
{"example.com/app", "com.example.app"},
{"www.example.com/app", "com.example.www.app"},
{"www.en.example.com/app", "com.example.en.www.app"},
{"example.com/dir/app", "com.example.app"},
{"example.com/dir.ext/app", "com.example.app"},
{"example.com/dir/app.ext", "com.example.app.ext"},
{"example-com.net/dir/app", "net.example_com.app"},
}
for i, test := range tests {
got := getAppID(&packageMetadata{PkgPath: test.in})
if exp := test.out; got != exp {
t.Errorf("(%d): expected '%s', got '%s'", i, exp, got)
}
}
}
-10
View File
@@ -1,10 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
/*
The gogio tool builds and packages Gio programs for Android, iOS/tvOS
and WebAssembly.
Run gogio with no arguments for instructions, or see the examples at
https://gioui.org.
*/
package main
-331
View File
@@ -1,331 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main_test
import (
"bufio"
"errors"
"flag"
"fmt"
"image"
"image/color"
"io"
"io/ioutil"
"os"
"os/exec"
"strings"
"testing"
"time"
)
var raceEnabled = false
var headless = flag.Bool("headless", true, "run end-to-end tests in headless mode")
const appid = "localhost.gogio.endtoend"
// TestDriver is implemented by each of the platforms we can run end-to-end
// tests on. None of its methods return any errors, as the errors are directly
// reported to testing.T via methods like Fatal.
type TestDriver interface {
initBase(t *testing.T, width, height int)
// Start opens the Gio app found at path. The driver should attempt to
// run the app with the base driver's width and height, and the
// platform's background should be white.
//
// When the function returns, the gio app must be ready to use on the
// platform, with its initial frame fully drawn.
Start(path string)
// Screenshot takes a screenshot of the Gio app on the platform.
Screenshot() image.Image
// Click performs a pointer click at the specified coordinates,
// including both press and release. It returns when the next frame is
// fully drawn.
Click(x, y int)
}
type driverBase struct {
*testing.T
width, height int
output io.Reader
frameNotifs chan bool
}
func (d *driverBase) initBase(t *testing.T, width, height int) {
d.T = t
d.width, d.height = width, height
}
func TestEndToEnd(t *testing.T) {
if testing.Short() {
t.Skipf("end-to-end tests tend to be slow")
}
t.Parallel()
const (
testdataWithGoImportPkgPath = "gioui.org/cmd/gogio/testdata"
testdataWithRelativePkgPath = "testdata/testdata.go"
)
// Keep this list local, to not reuse TestDriver objects.
subtests := []struct {
name string
driver TestDriver
pkgPath string
}{
{"X11 using go import path", &X11TestDriver{}, testdataWithGoImportPkgPath},
{"X11", &X11TestDriver{}, testdataWithRelativePkgPath},
// Doesn't work on the builders.
//{"Wayland", &WaylandTestDriver{}, testdataWithRelativePkgPath},
{"JS", &JSTestDriver{}, testdataWithRelativePkgPath},
{"Android", &AndroidTestDriver{}, testdataWithRelativePkgPath},
{"Windows", &WineTestDriver{}, testdataWithRelativePkgPath},
}
for _, subtest := range subtests {
t.Run(subtest.name, func(t *testing.T) {
subtest := subtest // copy the changing loop variable
t.Parallel()
runEndToEndTest(t, subtest.driver, subtest.pkgPath)
})
}
}
func runEndToEndTest(t *testing.T, driver TestDriver, pkgPath string) {
size := image.Point{X: 800, Y: 600}
driver.initBase(t, size.X, size.Y)
t.Log("starting driver and gio app")
driver.Start(pkgPath)
beef := color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff}
white := color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
black := color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}
gray := color.NRGBA{R: 0xbb, G: 0xbb, B: 0xbb, A: 0xff}
red := color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
// These are the four colors at the beginning.
t.Log("taking initial screenshot")
withRetries(t, 4*time.Second, func() error {
img := driver.Screenshot()
size = img.Bounds().Size() // override the default size
return checkImageCorners(img, beef, white, black, gray)
})
// TODO(mvdan): implement this properly in the Wayland driver; swaymsg
// almost works to automate clicks, but the button presses end up in the
// wrong coordinates.
if _, ok := driver.(*WaylandTestDriver); ok {
return
}
// Click the first and last sections to turn them red.
t.Log("clicking twice and taking another screenshot")
driver.Click(1*(size.X/4), 1*(size.Y/4))
driver.Click(3*(size.X/4), 3*(size.Y/4))
withRetries(t, 4*time.Second, func() error {
img := driver.Screenshot()
return checkImageCorners(img, red, white, black, red)
})
}
// withRetries keeps retrying fn until it succeeds, or until the timeout is hit.
// It uses a rudimentary kind of backoff, which starts with 100ms delays. As
// such, timeout should generally be in the order of seconds.
func withRetries(t *testing.T, timeout time.Duration, fn func() error) {
t.Helper()
timeoutTimer := time.NewTimer(timeout)
defer timeoutTimer.Stop()
backoff := 100 * time.Millisecond
tries := 0
var lastErr error
for {
if lastErr = fn(); lastErr == nil {
return
}
tries++
t.Logf("retrying after %s", backoff)
// Use a timer instead of a sleep, so that the timeout can stop
// the backoff early. Don't reuse this timer, since we're not in
// a hot loop, and we don't want tricky code.
backoffTimer := time.NewTimer(backoff)
defer backoffTimer.Stop()
select {
case <-timeoutTimer.C:
t.Errorf("last error: %v", lastErr)
t.Fatalf("hit timeout of %s after %d tries", timeout, tries)
case <-backoffTimer.C:
}
// Keep doubling it until a maximum. With the start at 100ms,
// we'll do: 100ms, 200ms, 400ms, 800ms, 1.6s, and 2s forever.
backoff *= 2
if max := 2 * time.Second; backoff > max {
backoff = max
}
}
}
type colorMismatch struct {
x, y int
wantRGB, gotRGB [3]uint32
}
func (m colorMismatch) String() string {
return fmt.Sprintf("%3d,%-3d got 0x%04x%04x%04x, want 0x%04x%04x%04x",
m.x, m.y,
m.gotRGB[0], m.gotRGB[1], m.gotRGB[2],
m.wantRGB[0], m.wantRGB[1], m.wantRGB[2],
)
}
func checkImageCorners(img image.Image, topLeft, topRight, botLeft, botRight color.Color) error {
// The colors are split in four rectangular sections. Check the corners
// of each of the sections. We check the corners left to right, top to
// bottom, like when reading left-to-right text.
size := img.Bounds().Size()
var mismatches []colorMismatch
checkColor := func(x, y int, want color.Color) {
r, g, b, _ := want.RGBA()
got := img.At(x, y)
r_, g_, b_, _ := got.RGBA()
if r_ != r || g_ != g || b_ != b {
mismatches = append(mismatches, colorMismatch{
x: x,
y: y,
wantRGB: [3]uint32{r, g, b},
gotRGB: [3]uint32{r_, g_, b_},
})
}
}
{
minX, minY := 5, 5
maxX, maxY := (size.X/2)-5, (size.Y/2)-5
checkColor(minX, minY, topLeft)
checkColor(maxX, minY, topLeft)
checkColor(minX, maxY, topLeft)
checkColor(maxX, maxY, topLeft)
}
{
minX, minY := (size.X/2)+5, 5
maxX, maxY := size.X-5, (size.Y/2)-5
checkColor(minX, minY, topRight)
checkColor(maxX, minY, topRight)
checkColor(minX, maxY, topRight)
checkColor(maxX, maxY, topRight)
}
{
minX, minY := 5, (size.Y/2)+5
maxX, maxY := (size.X/2)-5, size.Y-5
checkColor(minX, minY, botLeft)
checkColor(maxX, minY, botLeft)
checkColor(minX, maxY, botLeft)
checkColor(maxX, maxY, botLeft)
}
{
minX, minY := (size.X/2)+5, (size.Y/2)+5
maxX, maxY := size.X-5, size.Y-5
checkColor(minX, minY, botRight)
checkColor(maxX, minY, botRight)
checkColor(minX, maxY, botRight)
checkColor(maxX, maxY, botRight)
}
if n := len(mismatches); n > 0 {
b := new(strings.Builder)
fmt.Fprintf(b, "encountered %d color mismatches:\n", n)
for _, m := range mismatches {
fmt.Fprintf(b, "%s\n", m)
}
return errors.New(b.String())
}
return nil
}
func (d *driverBase) waitForFrame() {
d.Helper()
if d.frameNotifs == nil {
// Start the goroutine that reads output lines and notifies of
// new frames via frameNotifs. The test doesn't wait for this
// goroutine to finish; it will naturally end when the output
// reader reaches an error like EOF.
d.frameNotifs = make(chan bool, 1)
if d.output == nil {
d.Fatal("need an output reader to be notified of frames")
}
go func() {
scanner := bufio.NewScanner(d.output)
for scanner.Scan() {
line := scanner.Text()
d.Log(line)
if strings.Contains(line, "gio frame ready") {
d.frameNotifs <- true
}
}
// Since we're only interested in the output while the
// app runs, and we don't know when it finishes here,
// ignore "already closed" pipe errors.
if err := scanner.Err(); err != nil && !errors.Is(err, os.ErrClosed) {
d.Errorf("reading app output: %v", err)
}
}()
}
// Unfortunately, there isn't a way to select on a test failing, since
// testing.T doesn't have anything like a context or a "done" channel.
//
// We can't let selects block forever, since the default -test.timeout
// is ten minutes - far too long for tests that take seconds.
//
// For now, a static short timeout is better than nothing. 5s is plenty
// for our simple test app to render on any device.
select {
case <-d.frameNotifs:
case <-time.After(5 * time.Second):
d.Fatalf("timed out waiting for a frame to be ready")
}
}
func (d *driverBase) needPrograms(names ...string) {
d.Helper()
for _, name := range names {
if _, err := exec.LookPath(name); err != nil {
d.Skipf("%s needed to run", name)
}
}
}
func (d *driverBase) tempDir(name string) string {
d.Helper()
dir, err := ioutil.TempDir("", name)
if err != nil {
d.Fatal(err)
}
d.Cleanup(func() { os.RemoveAll(dir) })
return dir
}
func (d *driverBase) gogio(args ...string) {
d.Helper()
prog, err := os.Executable()
if err != nil {
d.Fatal(err)
}
cmd := exec.Command(prog, args...)
cmd.Env = append(os.Environ(), "RUN_GOGIO=1")
if out, err := cmd.CombinedOutput(); err != nil {
d.Fatalf("gogio error: %s:\n%s", err, out)
}
}
-69
View File
@@ -1,69 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main
const mainUsage = `The gogio command builds and packages Gio (gioui.org) programs.
Usage:
gogio -target <target> [flags] <package> [run arguments]
The gogio tool builds and packages Gio programs for platforms where additional
metadata or support files are required.
The package argument specifies an import path or a single Go source file to
package. Any run arguments are appended to os.Args at runtime.
Compiled Java class files from jar files in the package directory are
included in Android builds.
The mandatory -target flag selects the target platform: ios or android for the
mobile platforms, tvos for Apple's tvOS, js for WebAssembly/WebGL.
The -arch flag specifies a comma separated list of GOARCHs to include. The
default is all supported architectures.
The -o flag specifies an output file or directory, depending on the target.
The -buildmode flag selects the build mode. Two build modes are available, exe
and archive. Buildmode exe outputs an .ipa file for iOS or tvOS, an .apk file
for Android or a directory with the WebAssembly module and support files for
a browser.
The -ldflags and -tags flags pass extra linker flags and tags to the go tool.
As a special case for iOS or tvOS, specifying a path that ends with ".app"
will output an app directory suitable for a simulator.
The other buildmode is archive, which will output an .aar library for Android
or a .framework for iOS and tvOS.
The -icon flag specifies a path to a PNG image to use as app icon on iOS and Android.
If left unspecified, the appicon.png file from the main package is used
(if it exists).
The -appid flag specifies the package name for Android or the bundle id for
iOS and tvOS. A bundle id must be provisioned through Xcode before the gogio
tool can use it.
The -version flag specifies the integer version code for Android and the last
component of the 1.0.X version for iOS and tvOS.
For Android builds the -minsdk flag specify the minimum SDK level. For example,
use -minsdk 22 to target Android 5.1 (Lollipop) and later.
For Windows builds the -minsdk flag specify the minimum OS version. For example,
use -mindk 10 to target Windows 10 and later, -minsdk 6 for Windows Vista and later.
For iOS builds the -minsdk flag specify the minimum iOS version. For example,
use -mindk 15 to target iOS 15.0 and later.
The -work flag prints the path to the working directory and suppress
its deletion.
The -x flag will print all the external commands executed by the gogio tool.
The -signkey flag specifies the path of the keystore, used for signing Android apk/aab files.
The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided.
`
-589
View File
@@ -1,589 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main
import (
"archive/zip"
"crypto/sha1"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"golang.org/x/sync/errgroup"
)
const (
minIOSVersion = 10
// Metal is available from iOS 8 on devices, yet from version 13 on the
// simulator.
minSimulatorVersion = 13
)
func buildIOS(tmpDir, target string, bi *buildInfo) error {
appName := bi.name
switch *buildMode {
case "archive":
framework := *destPath
if framework == "" {
framework = fmt.Sprintf("%s.framework", strings.Title(appName))
}
return archiveIOS(tmpDir, target, framework, bi)
case "exe":
out := *destPath
if out == "" {
out = appName + ".ipa"
}
forDevice := strings.HasSuffix(out, ".ipa")
// Filter out unsupported architectures.
for i := len(bi.archs) - 1; i >= 0; i-- {
switch bi.archs[i] {
case "arm", "arm64":
if forDevice {
continue
}
case "386", "amd64":
if !forDevice {
continue
}
}
bi.archs = append(bi.archs[:i], bi.archs[i+1:]...)
}
tmpFramework := filepath.Join(tmpDir, "Gio.framework")
if err := archiveIOS(tmpDir, target, tmpFramework, bi); err != nil {
return err
}
if !forDevice && !strings.HasSuffix(out, ".app") {
return fmt.Errorf("the specified output directory %q does not end in .app or .ipa", out)
}
if !forDevice {
return exeIOS(tmpDir, target, out, bi)
}
payload := filepath.Join(tmpDir, "Payload")
appDir := filepath.Join(payload, appName+".app")
if err := os.MkdirAll(appDir, 0755); err != nil {
return err
}
if err := exeIOS(tmpDir, target, appDir, bi); err != nil {
return err
}
if err := signIOS(bi, tmpDir, appDir); err != nil {
return err
}
return zipDir(out, tmpDir, "Payload")
default:
panic("unreachable")
}
}
func signIOS(bi *buildInfo, tmpDir, app string) error {
home, err := os.UserHomeDir()
if err != nil {
return err
}
provPattern := filepath.Join(home, "Library", "MobileDevice", "Provisioning Profiles", "*.mobileprovision")
provisions, err := filepath.Glob(provPattern)
if err != nil {
return err
}
provInfo := filepath.Join(tmpDir, "provision.plist")
var avail []string
for _, prov := range provisions {
// Decode the provision file to a plist.
_, err := runCmd(exec.Command("security", "cms", "-D", "-i", prov, "-o", provInfo))
if err != nil {
return err
}
expUnix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:ExpirationDate", provInfo))
if err != nil {
return err
}
exp, err := time.Parse(time.UnixDate, expUnix)
if err != nil {
return fmt.Errorf("sign: failed to parse expiration date from %q: %v", prov, err)
}
if exp.Before(time.Now()) {
continue
}
appIDPrefix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:ApplicationIdentifierPrefix:0", provInfo))
if err != nil {
return err
}
provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:Entitlements:application-identifier", provInfo))
if err != nil {
return err
}
expAppID := fmt.Sprintf("%s.%s", appIDPrefix, bi.appID)
avail = append(avail, provAppID)
if expAppID != provAppID {
continue
}
// Copy provisioning file.
embedded := filepath.Join(app, "embedded.mobileprovision")
if err := copyFile(embedded, prov); err != nil {
return err
}
certDER, err := runCmdRaw(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:DeveloperCertificates:0", provInfo))
if err != nil {
return err
}
// Omit trailing newline.
certDER = certDER[:len(certDER)-1]
entitlements, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-x", "-c", "Print:Entitlements", provInfo))
if err != nil {
return err
}
entFile := filepath.Join(tmpDir, "entitlements.plist")
if err := ioutil.WriteFile(entFile, []byte(entitlements), 0660); err != nil {
return err
}
identity := sha1.Sum(certDER)
idHex := hex.EncodeToString(identity[:])
_, err = runCmd(exec.Command("codesign", "-s", idHex, "-v", "--entitlements", entFile, app))
return err
}
return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v", bi.appID, avail)
}
func exeIOS(tmpDir, target, app string, bi *buildInfo) error {
if bi.appID == "" {
return errors.New("app id is empty; use -appid to set it")
}
if err := os.RemoveAll(app); err != nil {
return err
}
if err := os.Mkdir(app, 0755); err != nil {
return err
}
mainm := filepath.Join(tmpDir, "main.m")
const mainmSrc = `@import UIKit;
@import Gio;
@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;
}
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([GioAppDelegate class]));
}
}`
if err := ioutil.WriteFile(mainm, []byte(mainmSrc), 0660); err != nil {
return err
}
appName := strings.Title(bi.name)
exe := filepath.Join(app, appName)
lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
var builds errgroup.Group
for _, a := range bi.archs {
clang, cflags, err := iosCompilerFor(target, a, bi.minsdk)
if err != nil {
return err
}
exeSlice := filepath.Join(tmpDir, "app-"+a)
lipo.Args = append(lipo.Args, exeSlice)
compile := exec.Command(clang, cflags...)
compile.Args = append(compile.Args,
"-Werror",
"-fmodules",
"-fobjc-arc",
"-x", "objective-c",
"-F", tmpDir,
"-o", exeSlice,
mainm,
)
builds.Go(func() error {
_, err := runCmd(compile)
return err
})
}
if err := builds.Wait(); err != nil {
return err
}
if _, err := runCmd(lipo); err != nil {
return err
}
infoPlist := buildInfoPlist(bi)
plistFile := filepath.Join(app, "Info.plist")
if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil {
return err
}
if _, err := os.Stat(bi.iconPath); err == nil {
assetPlist, err := iosIcons(bi, tmpDir, app, bi.iconPath)
if err != nil {
return err
}
// Merge assets plist with Info.plist
cmd := exec.Command(
"/usr/libexec/PlistBuddy",
"-c", "Merge "+assetPlist,
plistFile,
)
if _, err := runCmd(cmd); err != nil {
return err
}
}
if _, err := runCmd(exec.Command("plutil", "-convert", "binary1", plistFile)); err != nil {
return err
}
return nil
}
// iosIcons builds an asset catalog and compile it with the Xcode command actool.
// iosIcons returns the asset plist file to be merged into Info.plist.
func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) {
assets := filepath.Join(tmpDir, "Assets.xcassets")
if err := os.Mkdir(assets, 0700); err != nil {
return "", err
}
appIcon := filepath.Join(assets, "AppIcon.appiconset")
err := buildIcons(appIcon, icon, []iconVariant{
{path: "ios_2x.png", size: 120},
{path: "ios_3x.png", size: 180},
// The App Store icon is not allowed to contain
// transparent pixels.
{path: "ios_store.png", size: 1024, fill: true},
})
if err != nil {
return "", err
}
contentJson := `{
"images" : [
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "ios_2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "ios_3x.png",
"scale" : "3x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "ios_store.png",
"scale" : "1x"
}
]
}`
contentFile := filepath.Join(appIcon, "Contents.json")
if err := ioutil.WriteFile(contentFile, []byte(contentJson), 0600); err != nil {
return "", err
}
assetPlist := filepath.Join(tmpDir, "assets.plist")
minsdk := bi.minsdk
if minsdk == 0 {
minsdk = minIOSVersion
}
compile := exec.Command(
"actool",
"--compile", appDir,
"--platform", iosPlatformFor(bi.target),
"--minimum-deployment-target", strconv.Itoa(minsdk),
"--app-icon", "AppIcon",
"--output-partial-info-plist", assetPlist,
assets)
_, err = runCmd(compile)
return assetPlist, err
}
func buildInfoPlist(bi *buildInfo) string {
appName := strings.Title(bi.name)
platform := iosPlatformFor(bi.target)
var supportPlatform string
switch bi.target {
case "ios":
supportPlatform = "iPhoneOS"
case "tvos":
supportPlatform = "AppleTVOS"
}
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>%s</string>
<key>CFBundleIdentifier</key>
<string>%s</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>%s</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.%d</string>
<key>CFBundleVersion</key>
<string>%d</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array><string>arm64</string></array>
<key>DTPlatformName</key>
<string>%s</string>
<key>DTPlatformVersion</key>
<string>12.4</string>
<key>MinimumOSVersion</key>
<string>%d</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>%s</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>16G73</string>
<key>DTSDKBuild</key>
<string>16G73</string>
<key>DTSDKName</key>
<string>%s12.4</string>
<key>DTXcode</key>
<string>1030</string>
<key>DTXcodeBuild</key>
<string>10G8</string>
</dict>
</plist>`, appName, bi.appID, appName, bi.version, bi.version, platform, minIOSVersion, supportPlatform, platform)
}
func iosPlatformFor(target string) string {
switch target {
case "ios":
return "iphoneos"
case "tvos":
return "appletvos"
default:
panic("invalid platform " + target)
}
}
func archiveIOS(tmpDir, target, frameworkRoot string, bi *buildInfo) error {
framework := filepath.Base(frameworkRoot)
const suf = ".framework"
if !strings.HasSuffix(framework, suf) {
return fmt.Errorf("the specified output %q does not end in '.framework'", frameworkRoot)
}
framework = framework[:len(framework)-len(suf)]
if err := os.RemoveAll(frameworkRoot); err != nil {
return err
}
frameworkDir := filepath.Join(frameworkRoot, "Versions", "A")
for _, dir := range []string{"Headers", "Modules"} {
p := filepath.Join(frameworkDir, dir)
if err := os.MkdirAll(p, 0755); err != nil {
return err
}
}
symlinks := [][2]string{
{"Versions/Current/Headers", "Headers"},
{"Versions/Current/Modules", "Modules"},
{"Versions/Current/" + framework, framework},
{"A", filepath.Join("Versions", "Current")},
}
for _, l := range symlinks {
if err := os.Symlink(l[0], filepath.Join(frameworkRoot, l[1])); err != nil && !os.IsExist(err) {
return err
}
}
exe := filepath.Join(frameworkDir, framework)
lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
var builds errgroup.Group
tags := bi.tags
goos := "ios"
supportsIOS, err := supportsGOOS("ios")
if err != nil {
return err
}
if !supportsIOS {
// Go 1.15 and earlier target iOS with GOOS=darwin, tags=ios.
goos = "darwin"
tags = "ios " + tags
}
for _, a := range bi.archs {
clang, cflags, err := iosCompilerFor(target, a, bi.minsdk)
if err != nil {
return err
}
lib := filepath.Join(tmpDir, "gio-"+a)
cmd := exec.Command(
"go",
"build",
"-ldflags=-s -w "+bi.ldflags,
"-buildmode=c-archive",
"-o", lib,
"-tags", tags,
bi.pkgPath,
)
lipo.Args = append(lipo.Args, lib)
cflagsLine := strings.Join(cflags, " ")
cmd.Env = append(
os.Environ(),
"GOOS="+goos,
"GOARCH="+a,
"CGO_ENABLED=1",
"CC="+clang,
"CGO_CFLAGS="+cflagsLine,
"CGO_LDFLAGS="+cflagsLine,
)
builds.Go(func() error {
_, err := runCmd(cmd)
return err
})
}
if err := builds.Wait(); err != nil {
return err
}
if _, err := runCmd(lipo); err != nil {
return err
}
appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", "gioui.org/app/"))
if err != nil {
return err
}
headerDst := filepath.Join(frameworkDir, "Headers", framework+".h")
headerSrc := filepath.Join(appDir, "framework_ios.h")
if err := copyFile(headerDst, headerSrc); err != nil {
return err
}
module := fmt.Sprintf(`framework module "%s" {
header "%[1]s.h"
export *
}`, framework)
moduleFile := filepath.Join(frameworkDir, "Modules", "module.modulemap")
return ioutil.WriteFile(moduleFile, []byte(module), 0644)
}
func supportsGOOS(wantGoos string) (bool, error) {
geese, err := runCmd(exec.Command("go", "tool", "dist", "list"))
if err != nil {
return false, err
}
for _, pair := range strings.Split(geese, "\n") {
s := strings.SplitN(pair, "/", 2)
if len(s) != 2 {
return false, fmt.Errorf("go tool dist list: invalid GOOS/GOARCH pair: %s", pair)
}
goos := s[0]
if goos == wantGoos {
return true, nil
}
}
return false, nil
}
func iosCompilerFor(target, arch string, minsdk int) (string, []string, error) {
var (
platformSDK string
platformOS string
)
switch target {
case "ios":
platformOS = "ios"
platformSDK = "iphone"
case "tvos":
platformOS = "tvos"
platformSDK = "appletv"
}
switch arch {
case "arm", "arm64":
platformSDK += "os"
if minsdk == 0 {
minsdk = minIOSVersion
}
case "386", "amd64":
platformOS += "-simulator"
platformSDK += "simulator"
if minsdk == 0 {
minsdk = minSimulatorVersion
}
default:
return "", nil, fmt.Errorf("unsupported -arch: %s", arch)
}
sdkPath, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--show-sdk-path"))
if err != nil {
return "", nil, err
}
clang, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find", "clang"))
if err != nil {
return "", nil, err
}
cflags := []string{
"-fembed-bitcode",
"-arch", allArchs[arch].iosArch,
"-isysroot", sdkPath,
"-m" + platformOS + "-version-min=" + strconv.Itoa(minsdk),
}
return clang, cflags, nil
}
func zipDir(dst, base, dir string) (err error) {
f, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); err == nil {
err = cerr
}
}()
zipf := zip.NewWriter(f)
err = filepath.Walk(filepath.Join(base, dir), func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if f.IsDir() {
return nil
}
rel := filepath.ToSlash(path[len(base)+1:])
entry, err := zipf.Create(rel)
if err != nil {
return err
}
src, err := os.Open(path)
if err != nil {
return err
}
defer src.Close()
_, err = io.Copy(entry, src)
return err
})
if err != nil {
return err
}
return zipf.Close()
}
-123
View File
@@ -1,123 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main_test
import (
"bytes"
"context"
"errors"
"image"
"image/png"
"io"
"net/http"
"net/http/httptest"
"os/exec"
"github.com/chromedp/cdproto/runtime"
"github.com/chromedp/chromedp"
_ "gioui.org/unit" // the build tool adds it to go.mod, so keep it there
)
type JSTestDriver struct {
driverBase
// ctx is the chromedp context.
ctx context.Context
}
func (d *JSTestDriver) Start(path string) {
if raceEnabled {
d.Skipf("js/wasm doesn't support -race; skipping")
}
// First, build the app.
dir := d.tempDir("gio-endtoend-js")
d.gogio("-target=js", "-o="+dir, path)
// Second, start Chrome.
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", *headless),
)
actx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
d.Cleanup(cancel)
ctx, cancel := chromedp.NewContext(actx,
// Send all logf/errf calls to t.Logf
chromedp.WithLogf(d.Logf),
)
d.Cleanup(cancel)
d.ctx = ctx
if err := chromedp.Run(ctx); err != nil {
if errors.Is(err, exec.ErrNotFound) {
d.Skipf("test requires Chrome to be installed: %v", err)
return
}
d.Fatal(err)
}
pr, pw := io.Pipe()
d.Cleanup(func() { pw.Close() })
d.output = pr
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch ev := ev.(type) {
case *runtime.EventConsoleAPICalled:
switch ev.Type {
case "log", "info", "warning", "error":
var b bytes.Buffer
b.WriteString("console.")
b.WriteString(string(ev.Type))
b.WriteString("(")
for i, arg := range ev.Args {
if i > 0 {
b.WriteString(", ")
}
b.Write(arg.Value)
}
b.WriteString(")\n")
pw.Write(b.Bytes())
}
}
})
// Third, serve the app folder, set the browser tab dimensions, and
// navigate to the folder.
ts := httptest.NewServer(http.FileServer(http.Dir(dir)))
d.Cleanup(ts.Close)
if err := chromedp.Run(ctx,
chromedp.EmulateViewport(int64(d.width), int64(d.height)),
chromedp.Navigate(ts.URL),
); err != nil {
d.Fatal(err)
}
// Wait for the gio app to render.
d.waitForFrame()
}
func (d *JSTestDriver) Screenshot() image.Image {
var buf []byte
if err := chromedp.Run(d.ctx,
chromedp.CaptureScreenshot(&buf),
); err != nil {
d.Fatal(err)
}
img, err := png.Decode(bytes.NewReader(buf))
if err != nil {
d.Fatal(err)
}
return img
}
func (d *JSTestDriver) Click(x, y int) {
if err := chromedp.Run(d.ctx,
chromedp.MouseClickXY(float64(x), float64(y)),
); err != nil {
d.Fatal(err)
}
// Wait for the gio app to render after this click.
d.waitForFrame()
}
-201
View File
@@ -1,201 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
"golang.org/x/tools/go/packages"
)
func buildJS(bi *buildInfo) error {
out := *destPath
if out == "" {
out = bi.name
}
if err := os.MkdirAll(out, 0700); err != nil {
return err
}
cmd := exec.Command(
"go",
"build",
"-ldflags="+bi.ldflags,
"-tags="+bi.tags,
"-o", filepath.Join(out, "main.wasm"),
bi.pkgPath,
)
cmd.Env = append(
os.Environ(),
"GOOS=js",
"GOARCH=wasm",
)
_, err := runCmd(cmd)
if err != nil {
return err
}
var faviconPath string
if _, err := os.Stat(bi.iconPath); err == nil {
// Copy icon to the output folder
icon, err := ioutil.ReadFile(bi.iconPath)
if err != nil {
return err
}
if err := ioutil.WriteFile(filepath.Join(out, filepath.Base(bi.iconPath)), icon, 0600); err != nil {
return err
}
faviconPath = filepath.Base(bi.iconPath)
}
indexTemplate, err := template.New("").Parse(jsIndex)
if err != nil {
return err
}
var b bytes.Buffer
if err := indexTemplate.Execute(&b, struct {
Name string
Icon string
}{
Name: bi.name,
Icon: faviconPath,
}); err != nil {
return err
}
if err := ioutil.WriteFile(filepath.Join(out, "index.html"), b.Bytes(), 0600); err != nil {
return err
}
goroot, err := runCmd(exec.Command("go", "env", "GOROOT"))
if err != nil {
return err
}
wasmJS := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js")
if _, err := os.Stat(wasmJS); err != nil {
return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err)
}
pkgs, err := packages.Load(&packages.Config{
Mode: packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps,
Env: append(os.Environ(), "GOOS=js", "GOARCH=wasm"),
}, bi.pkgPath)
if err != nil {
return err
}
extraJS, err := findPackagesJS(pkgs[0], make(map[string]bool))
if err != nil {
return err
}
return mergeJSFiles(filepath.Join(out, "wasm.js"), append([]string{wasmJS}, extraJS...)...)
}
func findPackagesJS(p *packages.Package, visited map[string]bool) (extraJS []string, err error) {
if len(p.GoFiles) == 0 {
return nil, nil
}
js, err := filepath.Glob(filepath.Join(filepath.Dir(p.GoFiles[0]), "*_js.js"))
if err != nil {
return nil, err
}
extraJS = append(extraJS, js...)
for _, imp := range p.Imports {
if !visited[imp.ID] {
extra, err := findPackagesJS(imp, visited)
if err != nil {
return nil, err
}
extraJS = append(extraJS, extra...)
visited[imp.ID] = true
}
}
return extraJS, nil
}
// mergeJSFiles will merge all files into a single `wasm.js`. It will prepend the jsSetGo
// and append the jsStartGo.
func mergeJSFiles(dst string, files ...string) (err error) {
w, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
if cerr := w.Close(); err != nil {
err = cerr
}
}()
_, err = io.Copy(w, strings.NewReader(jsSetGo))
if err != nil {
return err
}
for i := range files {
r, err := os.Open(files[i])
if err != nil {
return err
}
_, err = io.Copy(w, r)
r.Close()
if err != nil {
return err
}
}
_, err = io.Copy(w, strings.NewReader(jsStartGo))
return err
}
const (
jsIndex = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
{{ if .Icon }}<link rel="icon" href="{{.Icon}}" type="image/x-icon" />{{ end }}
{{ if .Name }}<title>{{.Name}}</title>{{ end }}
<script src="wasm.js"></script>
<style>
body,pre { margin:0;padding:0; }
</style>
</head>
<body>
</body>
</html>`
// jsSetGo sets the `window.go` variable.
jsSetGo = `(() => {
window.go = {argv: [], env: {}, importObject: {go: {}}};
const argv = new URLSearchParams(location.search).get("argv");
if (argv) {
window.go["argv"] = argv.split(" ");
}
})();`
// jsStartGo initializes the main.wasm.
jsStartGo = `(() => {
defaultGo = new Go();
Object.assign(defaultGo["argv"], defaultGo["argv"].concat(go["argv"]));
Object.assign(defaultGo["env"], go["env"]);
for (let key in go["importObject"]) {
if (typeof defaultGo["importObject"][key] === "undefined") {
defaultGo["importObject"][key] = {};
}
Object.assign(defaultGo["importObject"][key], go["importObject"][key]);
}
window.go = defaultGo;
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
})();`
)
-224
View File
@@ -1,224 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"image"
"image/color"
"image/png"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/image/draw"
"golang.org/x/sync/errgroup"
)
var (
target = flag.String("target", "", "specify target (ios, tvos, android, js).\n")
archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).")
minsdk = flag.Int("minsdk", 0, "specify the minimum supported operating system level")
buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)")
destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.")
appID = flag.String("appid", "", "app identifier (for -buildmode=exe)")
version = flag.Int("version", 1, "app version (for -buildmode=exe)")
printCommands = flag.Bool("x", false, "print the commands")
keepWorkdir = flag.Bool("work", false, "print the name of the temporary work directory and do not delete it when exiting.")
linkMode = flag.String("linkmode", "", "set the -linkmode flag of the go tool")
extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker")
extraTags = flag.String("tags", "", "extra tags to the Go tool")
iconPath = flag.String("icon", "", "specify an icon for iOS and Android")
signKey = flag.String("signkey", "", "specify the path of the keystore to be used to sign Android apk files.")
signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.")
)
func main() {
flag.Usage = func() {
fmt.Fprint(os.Stderr, mainUsage)
}
flag.Parse()
if err := flagValidate(); err != nil {
fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
os.Exit(1)
}
buildInfo, err := newBuildInfo(flag.Arg(0))
if err != nil {
fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
os.Exit(1)
}
if err := build(buildInfo); err != nil {
fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
func flagValidate() error {
pkgPathArg := flag.Arg(0)
if pkgPathArg == "" {
return errors.New("specify a package")
}
if *target == "" {
return errors.New("please specify -target")
}
switch *target {
case "ios", "tvos", "android", "js", "windows":
default:
return fmt.Errorf("invalid -target %s", *target)
}
switch *buildMode {
case "archive", "exe":
default:
return fmt.Errorf("invalid -buildmode %s", *buildMode)
}
return nil
}
func build(bi *buildInfo) error {
tmpDir, err := ioutil.TempDir("", "gogio-")
if err != nil {
return err
}
if *keepWorkdir {
fmt.Fprintf(os.Stderr, "WORKDIR=%s\n", tmpDir)
} else {
defer os.RemoveAll(tmpDir)
}
switch *target {
case "js":
return buildJS(bi)
case "ios", "tvos":
return buildIOS(tmpDir, *target, bi)
case "android":
return buildAndroid(tmpDir, bi)
case "windows":
return buildWindows(tmpDir, bi)
default:
panic("unreachable")
}
}
func runCmdRaw(cmd *exec.Cmd) ([]byte, error) {
if *printCommands {
fmt.Printf("%s\n", strings.Join(cmd.Args, " "))
}
out, err := cmd.Output()
if err == nil {
return out, nil
}
if err, ok := err.(*exec.ExitError); ok {
return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr)
}
return nil, err
}
func runCmd(cmd *exec.Cmd) (string, error) {
out, err := runCmdRaw(cmd)
return string(bytes.TrimSpace(out)), err
}
func copyFile(dst, src string) (err error) {
r, err := os.Open(src)
if err != nil {
return err
}
defer r.Close()
w, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
if cerr := w.Close(); err == nil {
err = cerr
}
}()
_, err = io.Copy(w, r)
return err
}
type arch struct {
iosArch string
jniArch string
clangArch string
}
var allArchs = map[string]arch{
"arm": {
iosArch: "armv7",
jniArch: "armeabi-v7a",
clangArch: "armv7a-linux-androideabi",
},
"arm64": {
iosArch: "arm64",
jniArch: "arm64-v8a",
clangArch: "aarch64-linux-android",
},
"386": {
iosArch: "i386",
jniArch: "x86",
clangArch: "i686-linux-android",
},
"amd64": {
iosArch: "x86_64",
jniArch: "x86_64",
clangArch: "x86_64-linux-android",
},
}
type iconVariant struct {
path string
size int
fill bool
}
func buildIcons(baseDir, icon string, variants []iconVariant) error {
f, err := os.Open(icon)
if err != nil {
return err
}
defer f.Close()
img, _, err := image.Decode(f)
if err != nil {
return err
}
var resizes errgroup.Group
for _, v := range variants {
v := v
resizes.Go(func() (err error) {
path := filepath.Join(baseDir, v.path)
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); err == nil {
err = cerr
}
}()
return png.Encode(f, resizeIcon(v, img))
})
}
return resizes.Wait()
}
func resizeIcon(v iconVariant, img image.Image) *image.NRGBA {
scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}})
op := draw.Src
if v.fill {
op = draw.Over
draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
}
draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil)
return scaled
}
-17
View File
@@ -1,17 +0,0 @@
package main
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
if os.Getenv("RUN_GOGIO") != "" {
// Allow the end-to-end tests to call the gogio tool without
// having to build it from scratch, nor having to refactor the
// main function to avoid using global variables.
main()
os.Exit(0) // main already exits, but just in case.
}
os.Exit(m.Run())
}
-33
View File
@@ -1,33 +0,0 @@
package main
var AndroidPermissions = map[string][]string{
"network": {
"android.permission.INTERNET",
},
"networkstate": {
"android.permission.ACCESS_NETWORK_STATE",
},
"bluetooth": {
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.ACCESS_FINE_LOCATION",
},
"camera": {
"android.permission.CAMERA",
},
"storage": {
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
},
}
var AndroidFeatures = map[string][]string{
"default": {`glEsVersion="0x00020000"`, `name="android.hardware.type.pc"`},
"bluetooth": {
`name="android.hardware.bluetooth"`,
`name="android.hardware.bluetooth_le"`,
},
"camera": {
`name="android.hardware.camera"`,
},
}
-8
View File
@@ -1,8 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
//go:build race
// +build race
package main_test
func init() { raceEnabled = true }
-142
View File
@@ -1,142 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
// A simple app used for gogio's end-to-end tests.
package main
import (
"fmt"
"image"
"image/color"
"log"
"gioui.org/app"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
)
func main() {
go func() {
w := app.NewWindow()
if err := loop(w); err != nil {
log.Fatal(err)
}
}()
app.Main()
}
type notifyFrame int
const (
notifyNone notifyFrame = iota
notifyInvalidate
notifyPrint
)
// notify keeps track of whether we want to print to stdout to notify the user
// when a frame is ready. Initially we want to notify about the first frame.
var notify = notifyInvalidate
type (
C = layout.Context
D = layout.Dimensions
)
func loop(w *app.Window) error {
topLeft := quarterWidget{
color: color.NRGBA{R: 0xde, G: 0xad, B: 0xbe, A: 0xff},
}
topRight := quarterWidget{
color: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff},
}
botLeft := quarterWidget{
color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff},
}
botRight := quarterWidget{
color: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x80},
}
var ops op.Ops
for {
e := <-w.Events()
switch e := e.(type) {
case system.DestroyEvent:
return e.Err
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
// Clear background to white, even on embedded platforms such as webassembly.
paint.Fill(gtx.Ops, color.NRGBA{A: 0xff, R: 0xff, G: 0xff, B: 0xff})
layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
// r1c1
layout.Flexed(1, func(gtx C) D { return topLeft.Layout(gtx) }),
// r1c2
layout.Flexed(1, func(gtx C) D { return topRight.Layout(gtx) }),
)
}),
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
// r2c1
layout.Flexed(1, func(gtx C) D { return botLeft.Layout(gtx) }),
// r2c2
layout.Flexed(1, func(gtx C) D { return botRight.Layout(gtx) }),
)
}),
)
e.Frame(gtx.Ops)
switch notify {
case notifyInvalidate:
notify = notifyPrint
w.Invalidate()
case notifyPrint:
notify = notifyNone
fmt.Println("gio frame ready")
}
}
}
}
// quarterWidget paints a quarter of the screen with one color. When clicked, it
// turns red, going back to its normal color when clicked again.
type quarterWidget struct {
color color.NRGBA
clicked bool
}
var red = color.NRGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}
func (w *quarterWidget) Layout(gtx layout.Context) layout.Dimensions {
var color color.NRGBA
if w.clicked {
color = red
} else {
color = w.color
}
r := image.Rectangle{Max: gtx.Constraints.Max}
paint.FillShape(gtx.Ops, color, clip.Rect(r).Op())
defer clip.Rect(image.Rectangle{
Max: image.Pt(gtx.Constraints.Max.X, gtx.Constraints.Max.Y),
}).Push(gtx.Ops).Pop()
pointer.InputOp{
Tag: w,
Types: pointer.Press,
}.Add(gtx.Ops)
for _, e := range gtx.Events(w) {
if e, ok := e.(pointer.Event); ok && e.Type == pointer.Press {
w.clicked = !w.clicked
// notify when we're done updating the frame.
notify = notifyInvalidate
}
}
return layout.Dimensions{Size: gtx.Constraints.Max}
}
-196
View File
@@ -1,196 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main_test
import (
"bufio"
"bytes"
"context"
"fmt"
"image"
"image/png"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"text/template"
"time"
)
type WaylandTestDriver struct {
driverBase
runtimeDir string
socket string
display string
}
// No bars or anything fancy. Just a white background with our dimensions.
var tmplSwayConfig = template.Must(template.New("").Parse(`
output * bg #FFFFFF solid_color
output * mode {{.Width}}x{{.Height}}
default_border none
`))
var rxSwayReady = regexp.MustCompile(`Running compositor on wayland display '(.*)'`)
func (d *WaylandTestDriver) Start(path string) {
// We want os.Environ, so that it can e.g. find $DISPLAY to run within
// X11. wlroots env vars are documented at:
// https://github.com/swaywm/wlroots/blob/master/docs/env_vars.md
env := os.Environ()
if *headless {
env = append(env, "WLR_BACKENDS=headless")
}
d.needPrograms(
"sway", // to run a wayland compositor
"grim", // to take screenshots
"swaymsg", // to send input
)
// First, build the app.
dir := d.tempDir("gio-endtoend-wayland")
bin := filepath.Join(dir, "red")
flags := []string{"build", "-tags", "nox11", "-o=" + bin}
if raceEnabled {
flags = append(flags, "-race")
}
flags = append(flags, path)
cmd := exec.Command("go", flags...)
if out, err := cmd.CombinedOutput(); err != nil {
d.Fatalf("could not build app: %s:\n%s", err, out)
}
conf := filepath.Join(dir, "config")
f, err := os.Create(conf)
if err != nil {
d.Fatal(err)
}
defer f.Close()
if err := tmplSwayConfig.Execute(f, struct{ Width, Height int }{
d.width, d.height,
}); err != nil {
d.Fatal(err)
}
d.socket = filepath.Join(dir, "socket")
env = append(env, "SWAYSOCK="+d.socket)
d.runtimeDir = dir
env = append(env, "XDG_RUNTIME_DIR="+d.runtimeDir)
var wg sync.WaitGroup
d.Cleanup(wg.Wait)
// First, start sway.
{
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, "sway", "--config", conf, "--verbose")
cmd.Env = env
stderr, err := cmd.StderrPipe()
if err != nil {
d.Fatal(err)
}
if err := cmd.Start(); err != nil {
d.Fatal(err)
}
d.Cleanup(cancel)
d.Cleanup(func() {
// Give it a chance to exit gracefully, cleaning up
// after itself. After 10ms, the deferred cancel above
// will signal an os.Kill.
cmd.Process.Signal(os.Interrupt)
time.Sleep(10 * time.Millisecond)
})
// Wait for sway to be ready. We probably don't need a deadline
// here.
br := bufio.NewReader(stderr)
for {
line, err := br.ReadString('\n')
if err != nil {
d.Fatal(err)
}
if m := rxSwayReady.FindStringSubmatch(line); m != nil {
d.display = m[1]
break
}
}
wg.Add(1)
go func() {
if err := cmd.Wait(); err != nil && ctx.Err() == nil && !strings.Contains(err.Error(), "interrupt") {
// Don't print all stderr, since we use --verbose.
// TODO(mvdan): if it's useful, probably filter
// errors and show them.
d.Error(err)
}
wg.Done()
}()
}
// Then, start our program on the sway compositor above.
{
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, bin)
cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
output, err := cmd.StdoutPipe()
if err != nil {
d.Fatal(err)
}
cmd.Stderr = cmd.Stdout
d.output = output
if err := cmd.Start(); err != nil {
d.Fatal(err)
}
d.Cleanup(cancel)
wg.Add(1)
go func() {
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
d.Error(err)
}
wg.Done()
}()
}
// Wait for the gio app to render.
d.waitForFrame()
}
func (d *WaylandTestDriver) Screenshot() image.Image {
cmd := exec.Command("grim", "/dev/stdout")
cmd.Env = []string{"XDG_RUNTIME_DIR=" + d.runtimeDir, "WAYLAND_DISPLAY=" + d.display}
out, err := cmd.CombinedOutput()
if err != nil {
d.Errorf("%s", out)
d.Fatal(err)
}
img, err := png.Decode(bytes.NewReader(out))
if err != nil {
d.Fatal(err)
}
return img
}
func (d *WaylandTestDriver) swaymsg(args ...interface{}) {
strs := []string{"--socket", d.socket}
for _, arg := range args {
strs = append(strs, fmt.Sprint(arg))
}
cmd := exec.Command("swaymsg", strs...)
if out, err := cmd.CombinedOutput(); err != nil {
d.Errorf("%s", out)
d.Fatal(err)
}
}
func (d *WaylandTestDriver) Click(x, y int) {
d.swaymsg("seat", "-", "cursor", "set", x, y)
d.swaymsg("seat", "-", "cursor", "press", "button1")
d.swaymsg("seat", "-", "cursor", "release", "button1")
// Wait for the gio app to render after this click.
d.waitForFrame()
}
-152
View File
@@ -1,152 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main_test
import (
"context"
"image"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"sync"
"time"
"golang.org/x/image/draw"
)
// Wine is tightly coupled with X11 at the moment, and we can reuse the same
// methods to automate screenshots and clicks. The main difference is how we
// build and run the app.
// The only quirk is that it seems impossible for the Wine window to take the
// entirety of the X server's dimensions, even if we try to resize it to take
// the entire display. It seems to want to leave some vertical space empty,
// presumably for window decorations or the "start" bar on Windows. To work
// around that, make the X server 50x50px bigger, and crop the screenshots back
// to the original size.
type WineTestDriver struct {
X11TestDriver
}
func (d *WineTestDriver) Start(path string) {
d.needPrograms("wine")
// First, build the app.
bin := filepath.Join(d.tempDir("gio-endtoend-windows"), "red.exe")
flags := []string{"build", "-o=" + bin}
if raceEnabled {
if runtime.GOOS != "windows" {
// cross-compilation disables CGo, which breaks -race.
d.Skipf("can't cross-compile -race for Windows; skipping")
}
flags = append(flags, "-race")
}
flags = append(flags, path)
cmd := exec.Command("go", flags...)
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, "GOOS=windows")
if out, err := cmd.CombinedOutput(); err != nil {
d.Fatalf("could not build app: %s:\n%s", err, out)
}
var wg sync.WaitGroup
d.Cleanup(wg.Wait)
// Add 50x50px to the display dimensions, as discussed earlier.
d.startServer(&wg, d.width+50, d.height+50)
// Then, start our program via Wine on the X server above.
{
cacheDir, err := os.UserCacheDir()
if err != nil {
d.Fatal(err)
}
// Use a wine directory separate from the default ~/.wine, so
// that the user's winecfg doesn't affect our test. This will
// default to ~/.cache/gio-e2e-wine. We use the user's cache,
// to reuse a previously set up wineprefix.
wineprefix := filepath.Join(cacheDir, "gio-e2e-wine")
// First, ensure that wineprefix is up to date with wineboot.
// Wait for this separately from the first frame, as setting up
// a new prefix might take 5s on its own.
env := []string{
"DISPLAY=" + d.display,
"WINEDEBUG=fixme-all", // hide "fixme" noise
"WINEPREFIX=" + wineprefix,
// Disable wine-gecko (Explorer) and wine-mono (.NET).
// Otherwise, if not installed, wineboot will get stuck
// with a prompt to install them on the virtual X
// display. Moreover, Gio doesn't need either, and wine
// is faster without them.
"WINEDLLOVERRIDES=mscoree,mshtml=",
}
{
start := time.Now()
cmd := exec.Command("wine", "wineboot", "-i")
cmd.Env = env
// Use a combined output pipe instead of CombinedOutput,
// so that we only wait for the child process to exit,
// and we don't need to wait for all of wine's
// grandchildren to exit and stop writing. This is
// relevant as wine leaves "wineserver" lingering for
// three seconds by default, to be reused later.
stdout, err := cmd.StdoutPipe()
if err != nil {
d.Fatal(err)
}
cmd.Stderr = cmd.Stdout
if err := cmd.Run(); err != nil {
io.Copy(os.Stderr, stdout)
d.Fatal(err)
}
d.Logf("set up WINEPREFIX in %s", time.Since(start))
}
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, "wine", bin)
cmd.Env = env
output, err := cmd.StdoutPipe()
if err != nil {
d.Fatal(err)
}
cmd.Stderr = cmd.Stdout
d.output = output
if err := cmd.Start(); err != nil {
d.Fatal(err)
}
d.Cleanup(cancel)
wg.Add(1)
go func() {
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
d.Error(err)
}
wg.Done()
}()
}
// Wait for the gio app to render.
d.waitForFrame()
// xdotool seems to fail at actually moving the window if we use it
// immediately after Gio is ready. Why?
// We can't tell if the windowmove operation worked until we take a
// screenshot, because the getwindowgeometry op reports the 0x0
// coordinates even if the window wasn't moved properly.
// A sleep of ~20ms seems to be enough on an idle laptop. Use 20x that.
// TODO(mvdan): revisit this, when you have a spare three hours.
time.Sleep(400 * time.Millisecond)
id := d.xdotool("search", "--sync", "--onlyvisible", "--name", "Gio")
d.xdotool("windowmove", "--sync", id, 0, 0)
}
func (d *WineTestDriver) Screenshot() image.Image {
img := d.X11TestDriver.Screenshot()
// Crop the screenshot back to the original dimensions.
cropped := image.NewRGBA(image.Rect(0, 0, d.width, d.height))
draw.Draw(cropped, cropped.Bounds(), img, image.Point{}, draw.Src)
return cropped
}
-416
View File
@@ -1,416 +0,0 @@
package main
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"image/png"
"io"
"math"
"os"
"os/exec"
"path/filepath"
"reflect"
"strconv"
"strings"
"text/template"
"github.com/akavel/rsrc/binutil"
"github.com/akavel/rsrc/coff"
"golang.org/x/text/encoding/unicode"
)
func buildWindows(tmpDir string, bi *buildInfo) error {
builder := &windowsBuilder{TempDir: tmpDir}
builder.DestDir = *destPath
if builder.DestDir == "" {
builder.DestDir = bi.pkgPath
}
name := bi.name
if *destPath != "" {
if filepath.Ext(*destPath) != ".exe" {
return fmt.Errorf("invalid output name %q, it must end with `.exe`", *destPath)
}
name = filepath.Base(*destPath)
}
name = strings.TrimSuffix(name, ".exe")
sdk := bi.minsdk
if sdk > 10 {
return fmt.Errorf("invalid minsdk (%d) it's higher than Windows 10", sdk)
}
version := strconv.Itoa(bi.version)
if bi.version > math.MaxUint16 {
return fmt.Errorf("version (%d) is larger than the maximum (%d)", bi.version, math.MaxUint16)
}
for _, arch := range bi.archs {
builder.Coff = coff.NewRSRC()
builder.Coff.Arch(arch)
if err := builder.embedIcon(bi.iconPath); err != nil {
return err
}
if err := builder.embedManifest(windowsManifest{
Version: "1.0.0." + version,
WindowsVersion: sdk,
Name: name,
}); err != nil {
return fmt.Errorf("can't create manifest: %v", err)
}
if err := builder.embedInfo(windowsResources{
Version: [2]uint32{uint32(1) << 16, uint32(bi.version)},
VersionHuman: "1.0.0." + version,
Name: name,
Language: 0x0400, // Process Default Language: https://docs.microsoft.com/en-us/previous-versions/ms957130(v=msdn.10)
}); err != nil {
return fmt.Errorf("can't create info: %v", err)
}
if err := builder.buildResource(bi, name, arch); err != nil {
return fmt.Errorf("can't build the resources: %v", err)
}
if err := builder.buildProgram(bi, name, arch); err != nil {
return err
}
}
return nil
}
type (
windowsResources struct {
Version [2]uint32
VersionHuman string
Language uint16
Name string
}
windowsManifest struct {
Version string
WindowsVersion int
Name string
}
windowsBuilder struct {
TempDir string
DestDir string
Coff *coff.Coff
}
)
const (
// https://docs.microsoft.com/en-us/windows/win32/menurc/resource-types
windowsResourceIcon = 3
windowsResourceIconGroup = windowsResourceIcon + 11
windowsResourceManifest = 24
windowsResourceVersion = 16
)
type bufferCoff struct {
bytes.Buffer
}
func (b *bufferCoff) Size() int64 {
return int64(b.Len())
}
func (b *windowsBuilder) embedIcon(path string) (err error) {
iconFile, err := os.Open(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("can't read the icon located at %s: %v", path, err)
}
defer iconFile.Close()
iconImage, err := png.Decode(iconFile)
if err != nil {
return fmt.Errorf("can't decode the PNG file (%s): %v", path, err)
}
sizes := []int{16, 32, 48, 64, 128, 256}
var iconHeader bufferCoff
// GRPICONDIR structure.
if err := binary.Write(&iconHeader, binary.LittleEndian, [3]uint16{0, 1, uint16(len(sizes))}); err != nil {
return err
}
for _, size := range sizes {
var iconBuffer bufferCoff
if err := png.Encode(&iconBuffer, resizeIcon(iconVariant{size: size, fill: false}, iconImage)); err != nil {
return fmt.Errorf("can't encode image: %v", err)
}
b.Coff.AddResource(windowsResourceIcon, uint16(size), &iconBuffer)
if err := binary.Write(&iconHeader, binary.LittleEndian, struct {
Size [2]uint8
Color [2]uint8
Planes uint16
BitCount uint16
Length uint32
Id uint16
}{
Size: [2]uint8{uint8(size % 256), uint8(size % 256)}, // "0" means 256px.
Planes: 1,
BitCount: 32,
Length: uint32(iconBuffer.Len()),
Id: uint16(size),
}); err != nil {
return err
}
}
b.Coff.AddResource(windowsResourceIconGroup, 1, &iconHeader)
return nil
}
func (b *windowsBuilder) buildResource(buildInfo *buildInfo, name string, arch string) error {
out, err := os.Create(filepath.Join(buildInfo.pkgPath, name+"_windows_"+arch+".syso"))
if err != nil {
return err
}
defer out.Close()
b.Coff.Freeze()
// See https://github.com/akavel/rsrc/internal/write.go#L13.
w := binutil.Writer{W: out}
binutil.Walk(b.Coff, func(v reflect.Value, path string) error {
if binutil.Plain(v.Kind()) {
w.WriteLE(v.Interface())
return nil
}
vv, ok := v.Interface().(binutil.SizedReader)
if ok {
w.WriteFromSized(vv)
return binutil.WALK_SKIP
}
return nil
})
if w.Err != nil {
return fmt.Errorf("error writing output file: %s", w.Err)
}
return nil
}
func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error {
dest := b.DestDir
if len(buildInfo.archs) > 1 {
dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe")
}
cmd := exec.Command(
"go",
"build",
"-ldflags=-H=windowsgui "+buildInfo.ldflags,
"-tags="+buildInfo.tags,
"-o", dest,
buildInfo.pkgPath,
)
cmd.Env = append(
os.Environ(),
"GOOS=windows",
"GOARCH="+arch,
)
_, err := runCmd(cmd)
return err
}
func (b *windowsBuilder) embedManifest(v windowsManifest) error {
t, err := template.New("manifest").Parse(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="{{.Name}}" version="{{.Version}}" />
<description>{{.Name}}</description>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
{{if (le .WindowsVersion 10)}}<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
{{end}}
{{if (le .WindowsVersion 9)}}<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
{{end}}
{{if (le .WindowsVersion 8)}}<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
{{end}}
{{if (le .WindowsVersion 7)}}<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
{{end}}
{{if (le .WindowsVersion 6)}}<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
{{end}}
</application>
</compatibility>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>`)
if err != nil {
return err
}
var manifest bufferCoff
if err := t.Execute(&manifest, v); err != nil {
return err
}
b.Coff.AddResource(windowsResourceManifest, 1, &manifest)
return nil
}
func (b *windowsBuilder) embedInfo(v windowsResources) error {
page := uint16(1)
// https://docs.microsoft.com/pt-br/windows/win32/menurc/vs-versioninfo
t := newValue(valueBinary, "VS_VERSION_INFO", []io.WriterTo{
// https://docs.microsoft.com/pt-br/windows/win32/api/VerRsrc/ns-verrsrc-vs_fixedfileinfo
windowsInfoValueFixed{
Signature: 0xFEEF04BD,
StructVersion: 0x00010000,
FileVersion: v.Version,
ProductVersion: v.Version,
FileFlagMask: 0x3F,
FileFlags: 0,
FileOS: 0x40004,
FileType: 0x1,
FileSubType: 0,
},
// https://docs.microsoft.com/pt-br/windows/win32/menurc/stringfileinfo
newValue(valueText, "StringFileInfo", []io.WriterTo{
// https://docs.microsoft.com/pt-br/windows/win32/menurc/stringtable
newValue(valueText, fmt.Sprintf("%04X%04X", v.Language, page), []io.WriterTo{
// https://docs.microsoft.com/pt-br/windows/win32/menurc/string-str
newValue(valueText, "ProductVersion", v.VersionHuman),
newValue(valueText, "FileVersion", v.VersionHuman),
newValue(valueText, "FileDescription", v.Name),
newValue(valueText, "ProductName", v.Name),
// TODO include more data: gogio must have some way to provide such information (like Company Name, Copyright...)
}),
}),
// https://docs.microsoft.com/pt-br/windows/win32/menurc/varfileinfo
newValue(valueBinary, "VarFileInfo", []io.WriterTo{
// https://docs.microsoft.com/pt-br/windows/win32/menurc/var-str
newValue(valueBinary, "Translation", uint32(page)<<16|uint32(v.Language)),
}),
})
// For some reason the ValueLength of the VS_VERSIONINFO must be the byte-length of `windowsInfoValueFixed`:
t.ValueLength = 52
var verrsrc bufferCoff
if _, err := t.WriteTo(&verrsrc); err != nil {
return err
}
b.Coff.AddResource(windowsResourceVersion, 1, &verrsrc)
return nil
}
type windowsInfoValueFixed struct {
Signature uint32
StructVersion uint32
FileVersion [2]uint32
ProductVersion [2]uint32
FileFlagMask uint32
FileFlags uint32
FileOS uint32
FileType uint32
FileSubType uint32
FileDate [2]uint32
}
func (v windowsInfoValueFixed) WriteTo(w io.Writer) (_ int64, err error) {
return 0, binary.Write(w, binary.LittleEndian, v)
}
type windowsInfoValue struct {
Length uint16
ValueLength uint16
Type uint16
Key []byte
Value []byte
}
func (v windowsInfoValue) WriteTo(w io.Writer) (_ int64, err error) {
// binary.Write doesn't support []byte inside struct.
if err = binary.Write(w, binary.LittleEndian, [3]uint16{v.Length, v.ValueLength, v.Type}); err != nil {
return 0, err
}
if _, err = w.Write(v.Key); err != nil {
return 0, err
}
if _, err = w.Write(v.Value); err != nil {
return 0, err
}
return 0, nil
}
const (
valueBinary uint16 = 0
valueText uint16 = 1
)
func newValue(valueType uint16, key string, input interface{}) windowsInfoValue {
v := windowsInfoValue{
Type: valueType,
Length: 6,
}
padding := func(in []byte) []byte {
if l := uint16(len(in)) + v.Length; l%4 != 0 {
return append(in, make([]byte, 4-l%4)...)
}
return in
}
v.Key = padding(utf16Encode(key))
v.Length += uint16(len(v.Key))
switch in := input.(type) {
case string:
v.Value = padding(utf16Encode(in))
v.ValueLength = uint16(len(v.Value) / 2)
case []io.WriterTo:
var buff bytes.Buffer
for k := range in {
if _, err := in[k].WriteTo(&buff); err != nil {
panic(err)
}
}
v.Value = buff.Bytes()
default:
var buff bytes.Buffer
if err := binary.Write(&buff, binary.LittleEndian, in); err != nil {
panic(err)
}
v.ValueLength = uint16(buff.Len())
v.Value = buff.Bytes()
}
v.Length += uint16(len(v.Value))
return v
}
// utf16Encode encodes the string to UTF16 with null-termination.
func utf16Encode(s string) []byte {
b, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder().Bytes([]byte(s))
if err != nil {
panic(err)
}
return append(b, 0x00, 0x00) // null-termination.
}
-170
View File
@@ -1,170 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main_test
import (
"bytes"
"context"
"fmt"
"image"
"image/png"
"io"
"math/rand"
"os"
"os/exec"
"path/filepath"
"sync"
"time"
)
type X11TestDriver struct {
driverBase
display string
}
func (d *X11TestDriver) Start(path string) {
// First, build the app.
bin := filepath.Join(d.tempDir("gio-endtoend-x11"), "red")
flags := []string{"build", "-tags", "nowayland", "-o=" + bin}
if raceEnabled {
flags = append(flags, "-race")
}
flags = append(flags, path)
cmd := exec.Command("go", flags...)
if out, err := cmd.CombinedOutput(); err != nil {
d.Fatalf("could not build app: %s:\n%s", err, out)
}
var wg sync.WaitGroup
d.Cleanup(wg.Wait)
d.startServer(&wg, d.width, d.height)
// Then, start our program on the X server above.
{
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, bin)
cmd.Env = []string{"DISPLAY=" + d.display}
output, err := cmd.StdoutPipe()
if err != nil {
d.Fatal(err)
}
cmd.Stderr = cmd.Stdout
d.output = output
if err := cmd.Start(); err != nil {
d.Fatal(err)
}
d.Cleanup(cancel)
wg.Add(1)
go func() {
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
d.Error(err)
}
wg.Done()
}()
}
// Wait for the gio app to render.
d.waitForFrame()
}
func (d *X11TestDriver) startServer(wg *sync.WaitGroup, width, height int) {
// Pick a random display number between 1 and 100,000. Most machines
// will only be using :0, so there's only a 0.001% chance of two
// concurrent test runs to run into a conflict.
rnd := rand.New(rand.NewSource(time.Now().UnixNano()))
d.display = fmt.Sprintf(":%d", rnd.Intn(100000)+1)
var xprog string
xflags := []string{
"-wr", // we want a white background; the default is black
}
if *headless {
xprog = "Xvfb" // virtual X server
xflags = append(xflags, "-screen", "0", fmt.Sprintf("%dx%dx24", width, height))
} else {
xprog = "Xephyr" // nested X server as a window
xflags = append(xflags, "-screen", fmt.Sprintf("%dx%d", width, height))
}
xflags = append(xflags, d.display)
d.needPrograms(
xprog, // to run the X server
"scrot", // to take screenshots
"xdotool", // to send input
)
ctx, cancel := context.WithCancel(context.Background())
cmd := exec.CommandContext(ctx, xprog, xflags...)
combined := &bytes.Buffer{}
cmd.Stdout = combined
cmd.Stderr = combined
if err := cmd.Start(); err != nil {
d.Fatal(err)
}
d.Cleanup(cancel)
d.Cleanup(func() {
// Give it a chance to exit gracefully, cleaning up
// after itself. After 10ms, the deferred cancel above
// will signal an os.Kill.
cmd.Process.Signal(os.Interrupt)
time.Sleep(10 * time.Millisecond)
})
// Wait for the X server to be ready. The socket path isn't
// terribly portable, but that's okay for now.
withRetries(d.T, time.Second, func() error {
socket := fmt.Sprintf("/tmp/.X11-unix/X%s", d.display[1:])
_, err := os.Stat(socket)
return err
})
wg.Add(1)
go func() {
if err := cmd.Wait(); err != nil && ctx.Err() == nil {
// Print all output and error.
io.Copy(os.Stdout, combined)
d.Error(err)
}
wg.Done()
}()
}
func (d *X11TestDriver) Screenshot() image.Image {
cmd := exec.Command("scrot", "--silent", "--overwrite", "/dev/stdout")
cmd.Env = []string{"DISPLAY=" + d.display}
out, err := cmd.CombinedOutput()
if err != nil {
d.Errorf("%s", out)
d.Fatal(err)
}
img, err := png.Decode(bytes.NewReader(out))
if err != nil {
d.Fatal(err)
}
return img
}
func (d *X11TestDriver) xdotool(args ...interface{}) string {
d.Helper()
strs := make([]string, len(args))
for i, arg := range args {
strs[i] = fmt.Sprint(arg)
}
cmd := exec.Command("xdotool", strs...)
cmd.Env = []string{"DISPLAY=" + d.display}
out, err := cmd.CombinedOutput()
if err != nil {
d.Errorf("%s", out)
d.Fatal(err)
}
return string(bytes.TrimSpace(out))
}
func (d *X11TestDriver) Click(x, y int) {
d.xdotool("mousemove", "--sync", x, y)
d.xdotool("click", "1")
// Wait for the gio app to render after this click.
d.waitForFrame()
}
+41 -3
View File
@@ -3,11 +3,11 @@
package f32 package f32
import ( import (
"fmt"
"math" "math"
"strconv"
) )
// Affine2D represents an affine 2D transformation. The zero value if Affine2D // Affine2D represents an affine 2D transformation. The zero value of Affine2D
// represents the identity transform. // represents the identity transform.
type Affine2D struct { type Affine2D struct {
// in order to make the zero value of Affine2D represent the identity // in order to make the zero value of Affine2D represent the identity
@@ -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. // Offset the transformation.
func (a Affine2D) Offset(offset Point) Affine2D { func (a Affine2D) Offset(offset Point) Affine2D {
return Affine2D{ return Affine2D{
@@ -112,6 +121,15 @@ func (a Affine2D) Elems() (sx, hx, ox, hy, sy, oy float32) {
return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f return a.a + 1, a.b, a.c, a.d, a.e + 1, a.f
} }
// Split a transform into two parts, one which is pure offset and the
// other representing the scaling, shearing and rotation part.
func (a Affine2D) Split() (srs Affine2D, offset Point) {
return Affine2D{
a: a.a, b: a.b, c: 0,
d: a.d, e: a.e, f: 0,
}, Point{X: a.c, Y: a.f}
}
func (a Affine2D) scale(factor Point) Affine2D { func (a Affine2D) scale(factor Point) Affine2D {
return Affine2D{ return Affine2D{
(a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X, (a.a+1)*factor.X - 1, a.b * factor.X, a.c * factor.X,
@@ -139,5 +157,25 @@ func (a Affine2D) shear(radiansX, radiansY float32) Affine2D {
func (a Affine2D) String() string { func (a Affine2D) String() string {
sx, hx, ox, hy, sy, oy := a.Elems() sx, hx, ox, hy, sy, oy := a.Elems()
return fmt.Sprintf("[[%f %f %f] [%f %f %f]]", sx, hx, ox, hy, sy, oy)
// precision 6, one period, negative sign and space per number
const prec = 6
const charsPerFloat = prec + 2 + 1
s := make([]byte, 0, 6*charsPerFloat+6)
s = append(s, '[', '[')
s = strconv.AppendFloat(s, float64(sx), 'g', prec, 32)
s = append(s, ' ')
s = strconv.AppendFloat(s, float64(hx), 'g', prec, 32)
s = append(s, ' ')
s = strconv.AppendFloat(s, float64(ox), 'g', prec, 32)
s = append(s, ']', ' ', '[')
s = strconv.AppendFloat(s, float64(hy), 'g', prec, 32)
s = append(s, ' ')
s = strconv.AppendFloat(s, float64(sy), 'g', prec, 32)
s = append(s, ' ')
s = strconv.AppendFloat(s, float64(oy), 'g', prec, 32)
s = append(s, ']', ']')
return string(s)
} }
+183 -30
View File
@@ -27,25 +27,51 @@ func TestTransformOffset(t *testing.T) {
p := Point{X: 1, Y: 2} p := Point{X: 1, Y: 2}
o := Point{X: 2, Y: -3} o := Point{X: 2, Y: -3}
r := Affine2D{}.Offset(o).Transform(p) r := AffineId().Offset(o).Transform(p)
if !eq(r, Pt(3, -1)) { if !eq(r, Pt(3, -1)) {
t.Errorf("offset transformation mismatch: have %v, want {3 -1}", r) 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) { if !eq(i, p) {
t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p) t.Errorf("offset transformation inverse mismatch: have %v, want %v", i, p)
} }
} }
func TestString(t *testing.T) {
tests := []struct {
in Affine2D
exp string
}{
{
in: NewAffine2D(9, 11, 13, 17, 19, 23),
exp: "[[9 11 13] [17 19 23]]",
}, {
in: NewAffine2D(29, 31, 37, 43, 47, 53),
exp: "[[29 31 37] [43 47 53]]",
}, {
in: NewAffine2D(29.142342, 31.4123412, 37.53152, 43.51324213, 47.123412, 53.14312342),
exp: "[[29.1423 31.4123 37.5315] [43.5132 47.1234 53.1431]]",
}, {
in: AffineId(),
exp: "[[1 0 0] [0 1 0]]",
},
}
for _, test := range tests {
if test.in.String() != test.exp {
t.Errorf("string mismatch: have %q, want %q", test.in.String(), test.exp)
}
}
}
func TestTransformScale(t *testing.T) { func TestTransformScale(t *testing.T) {
p := Point{X: 1, Y: 2} p := Point{X: 1, Y: 2}
s := 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)) { if !eq(r, Pt(-1, 4)) {
t.Errorf("scale transformation mismatch: have %v, want {-1 4}", r) 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) { if !eq(i, p) {
t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p) t.Errorf("scale transformation inverse mismatch: have %v, want %v", i, p)
} }
@@ -55,11 +81,11 @@ func TestTransformRotate(t *testing.T) {
p := Point{X: 1, Y: 0} p := Point{X: 1, Y: 0}
a := float32(math.Pi / 2) 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)) { if !eq(r, Pt(0, 1)) {
t.Errorf("rotate transformation mismatch: have %v, want {0 1}", r) 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) { if !eq(i, p) {
t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p) t.Errorf("rotate transformation inverse mismatch: have %v, want %v", i, p)
} }
@@ -68,11 +94,11 @@ func TestTransformRotate(t *testing.T) {
func TestTransformShear(t *testing.T) { func TestTransformShear(t *testing.T) {
p := Point{X: 1, Y: 1} 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)) { if !eq(r, Pt(2, 1)) {
t.Errorf("shear transformation mismatch: have %v, want {2 1}", r) 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) { if !eq(i, p) {
t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p) t.Errorf("shear transformation inverse mismatch: have %v, want %v", i, p)
} }
@@ -84,11 +110,11 @@ func TestTransformMultiply(t *testing.T) {
s := Point{X: -1, Y: 2} s := Point{X: -1, Y: 2}
a := float32(-math.Pi / 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)) { if !eq(r, Pt(1, 3)) {
t.Errorf("complex transformation mismatch: have %v, want {1 3}", r) 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) { if !eq(i, p) {
t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p) t.Errorf("complex transformation inverse mismatch: have %v, want %v", i, p)
} }
@@ -140,7 +166,7 @@ func TestPrimes(t *testing.T) {
func TestTransformScaleAround(t *testing.T) { func TestTransformScaleAround(t *testing.T) {
p := Pt(-1, -1) p := Pt(-1, -1)
target := Pt(-6, -13) 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) { if !eq(pt, target) {
t.Log(pt, "!=", target) t.Log(pt, "!=", target)
t.Error("Scale not as expected") t.Error("Scale not as expected")
@@ -149,7 +175,7 @@ func TestTransformScaleAround(t *testing.T) {
func TestTransformRotateAround(t *testing.T) { func TestTransformRotateAround(t *testing.T) {
p := Pt(-1, -1) 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) target := Pt(-1, 3)
if !eq(pt, target) { if !eq(pt, target) {
t.Log(pt, "!=", target) t.Log(pt, "!=", target)
@@ -158,12 +184,12 @@ func TestTransformRotateAround(t *testing.T) {
} }
func TestMulOrder(t *testing.T) { func TestMulOrder(t *testing.T) {
A := Affine2D{}.Offset(Pt(100, 100)) A := AffineId().Offset(Pt(100, 100))
B := Affine2D{}.Scale(Point{}, Pt(2, 2)) B := AffineId().Scale(Point{}, Pt(2, 2))
_ = A _ = A
_ = B _ = 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) T2 := B.Mul(A)
if T1 != T2 { if T1 != T2 {
@@ -176,9 +202,9 @@ func TestMulOrder(t *testing.T) {
func BenchmarkTransformOffset(b *testing.B) { func BenchmarkTransformOffset(b *testing.B) {
p := Point{X: 1, Y: 2} p := Point{X: 1, Y: 2}
o := Point{X: 0.5, Y: 0.5} 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 = aff.Transform(p)
} }
_ = p _ = p
@@ -187,8 +213,8 @@ func BenchmarkTransformOffset(b *testing.B) {
func BenchmarkTransformScale(b *testing.B) { func BenchmarkTransformScale(b *testing.B) {
p := Point{X: 1, Y: 2} p := Point{X: 1, Y: 2}
s := Point{X: 0.5, Y: 0.5} s := Point{X: 0.5, Y: 0.5}
aff := Affine2D{}.Scale(Point{}, s) aff := AffineId().Scale(Point{}, s)
for i := 0; i < b.N; i++ { for b.Loop() {
p = aff.Transform(p) p = aff.Transform(p)
} }
_ = p _ = p
@@ -197,36 +223,163 @@ func BenchmarkTransformScale(b *testing.B) {
func BenchmarkTransformRotate(b *testing.B) { func BenchmarkTransformRotate(b *testing.B) {
p := Point{X: 1, Y: 2} p := Point{X: 1, Y: 2}
a := float32(math.Pi / 2) a := float32(math.Pi / 2)
aff := Affine2D{}.Rotate(Point{}, a) aff := AffineId().Rotate(Point{}, a)
for i := 0; i < b.N; i++ { for b.Loop() {
p = aff.Transform(p) p = aff.Transform(p)
} }
_ = p _ = p
} }
func BenchmarkTransformTranslateMultiply(b *testing.B) { func BenchmarkTransformTranslateMultiply(b *testing.B) {
a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) a := AffineId().Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3)
t := Affine2D{}.Offset(Point{X: 0.5, Y: 0.5}) t := AffineId().Offset(Point{X: 0.5, Y: 0.5})
for i := 0; i < b.N; i++ { for b.Loop() {
a = a.Mul(t) a = a.Mul(t)
} }
} }
func BenchmarkTransformScaleMultiply(b *testing.B) { func BenchmarkTransformScaleMultiply(b *testing.B) {
a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) a := AffineId().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}) 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) a = a.Mul(t)
} }
} }
func BenchmarkTransformMultiply(b *testing.B) { func BenchmarkTransformMultiply(b *testing.B) {
a := Affine2D{}.Offset(Point{X: 1, Y: 1}).Rotate(Point{}, math.Pi/3) a := AffineId().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) 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) 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)
}
}
+6 -142
View File
@@ -2,7 +2,7 @@
/* /*
Package f32 is a float32 implementation of package image's Package f32 is a float32 implementation of package image's
Point and Rectangle. Point and affine transformations.
The coordinate space has the origin in the top left The coordinate space has the origin in the top left
corner with the axes extending right and down. corner with the axes extending right and down.
@@ -26,30 +26,6 @@ func (p Point) String() string {
"," + strconv.FormatFloat(float64(p.Y), 'f', -1, 32) + ")" "," + strconv.FormatFloat(float64(p.Y), 'f', -1, 32) + ")"
} }
// A Rectangle contains the points (X, Y) where Min.X <= X < Max.X,
// Min.Y <= Y < Max.Y.
type Rectangle struct {
Min, Max Point
}
// String return a string representation of r.
func (r Rectangle) String() string {
return r.Min.String() + "-" + r.Max.String()
}
// Rect is a shorthand for Rectangle{Point{x0, y0}, Point{x1, y1}}.
// The returned Rectangle has x0 and y0 swapped if necessary so that
// it's correctly formed.
func Rect(x0, y0, x1, y1 float32) Rectangle {
if x0 > x1 {
x0, x1 = x1, x0
}
if y0 > y1 {
y0, y1 = y1, y0
}
return Rectangle{Point{x0, y0}, Point{x1, y1}}
}
// Pt is shorthand for Point{X: x, Y: y}. // Pt is shorthand for Point{X: x, Y: y}.
func Pt(x, y float32) Point { func Pt(x, y float32) Point {
return Point{X: x, Y: y} return Point{X: x, Y: y}
@@ -75,122 +51,10 @@ func (p Point) Div(s float32) Point {
return Point{X: p.X / s, Y: p.Y / s} return Point{X: p.X / s, Y: p.Y / s}
} }
// In reports whether p is in r. // Round returns the integer point closest to p.
func (p Point) In(r Rectangle) bool { func (p Point) Round() image.Point {
return r.Min.X <= p.X && p.X < r.Max.X && return image.Point{
r.Min.Y <= p.Y && p.Y < r.Max.Y X: int(math.Round(float64(p.X))),
} Y: int(math.Round(float64(p.Y))),
// Size returns r's width and height.
func (r Rectangle) Size() Point {
return Point{X: r.Dx(), Y: r.Dy()}
}
// Dx returns r's width.
func (r Rectangle) Dx() float32 {
return r.Max.X - r.Min.X
}
// Dy returns r's Height.
func (r Rectangle) Dy() float32 {
return r.Max.Y - r.Min.Y
}
// Intersect returns the intersection of r and s.
func (r Rectangle) Intersect(s Rectangle) Rectangle {
if r.Min.X < s.Min.X {
r.Min.X = s.Min.X
}
if r.Min.Y < s.Min.Y {
r.Min.Y = s.Min.Y
}
if r.Max.X > s.Max.X {
r.Max.X = s.Max.X
}
if r.Max.Y > s.Max.Y {
r.Max.Y = s.Max.Y
}
if r.Empty() {
return Rectangle{}
}
return r
}
// Union returns the union of r and s.
func (r Rectangle) Union(s Rectangle) Rectangle {
if r.Empty() {
return s
}
if s.Empty() {
return r
}
if r.Min.X > s.Min.X {
r.Min.X = s.Min.X
}
if r.Min.Y > s.Min.Y {
r.Min.Y = s.Min.Y
}
if r.Max.X < s.Max.X {
r.Max.X = s.Max.X
}
if r.Max.Y < s.Max.Y {
r.Max.Y = s.Max.Y
}
return r
}
// Canon returns the canonical version of r, where Min is to
// the upper left of Max.
func (r Rectangle) Canon() Rectangle {
if r.Max.X < r.Min.X {
r.Min.X, r.Max.X = r.Max.X, r.Min.X
}
if r.Max.Y < r.Min.Y {
r.Min.Y, r.Max.Y = r.Max.Y, r.Min.Y
}
return r
}
// Empty reports whether r represents the empty area.
func (r Rectangle) Empty() bool {
return r.Min.X >= r.Max.X || r.Min.Y >= r.Max.Y
}
// Add offsets r with the vector p.
func (r Rectangle) Add(p Point) Rectangle {
return Rectangle{
Point{r.Min.X + p.X, r.Min.Y + p.Y},
Point{r.Max.X + p.X, r.Max.Y + p.Y},
} }
} }
// Sub offsets r with the vector -p.
func (r Rectangle) Sub(p Point) Rectangle {
return Rectangle{
Point{r.Min.X - p.X, r.Min.Y - p.Y},
Point{r.Max.X - p.X, r.Max.Y - p.Y},
}
}
// Round returns the smallest integer rectangle that
// contains r.
func (r Rectangle) Round() image.Rectangle {
return image.Rectangle{
Min: image.Point{
X: int(floor(r.Min.X)),
Y: int(floor(r.Min.Y)),
},
Max: image.Point{
X: int(ceil(r.Max.X)),
Y: int(ceil(r.Max.Y)),
},
}
}
func ceil(v float32) int {
return int(math.Ceil(float64(v)))
}
func floor(v float32) int {
return int(math.Floor(float64(v)))
}
Generated
+39 -92
View File
@@ -1,111 +1,58 @@
{ {
"nodes": { "nodes": {
"android": {
"inputs": {
"devshell": "devshell",
"flake-utils": "flake-utils_2",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1648412532,
"narHash": "sha256-zh0rLcppJ5i2Bh8oBWjBhDvgOrMhDGdXINbp3bhrs0U=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "b1318b23926260685dbb09dd127f38c917fc7441",
"type": "github"
},
"original": {
"owner": "tadfisher",
"repo": "android-nixpkgs",
"type": "github"
}
},
"devshell": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1647857022,
"narHash": "sha256-Aw70NWLOIwKhT60MHDGjgWis3DP3faCzr6ap9CSayek=",
"owner": "numtide",
"repo": "devshell",
"rev": "0a5ff74dacb9ea22614f64e61aeb3ca0bf0e7311",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1642700792,
"narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "846b2ae0fc4cc943637d3d1def4454213e203cba",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1648297722,
"narHash": "sha256-W+qlPsiZd8F3XkzXOzAoR+mpFqzm3ekQkJNa+PIh1BQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "0f8662f1319ad6abf89b3380dd2722369fc51ade",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1643381941, "lastModified": 1747953325,
"narHash": "sha256-pHTwvnN4tTsEKkWlXQ8JMY423epos8wUOhthpwJjtpc=", "narHash": "sha256-y2ZtlIlNTuVJUZCqzZAhIw5rrKP4DOSklev6c8PyCkQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5efc8ca954272c4376ac929f4c5ffefcc20551d5", "rev": "55d1f923c480dadce40f5231feb472e81b0bab48",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1648469949,
"narHash": "sha256-ExCG9k36QNs0bNsi2NwnfL4w/kjb661rW43pf03ok/Y=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a63a39e23873d5d72753ff12bb418cb7e470790c",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
}, },
"root": { "root": {
"inputs": { "inputs": {
"android": "android", "nixpkgs": "nixpkgs",
"nixpkgs": "nixpkgs_2" "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 -41
View File
@@ -3,42 +3,38 @@
description = "Gio build environment"; description = "Gio build environment";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
android.url = "github:tadfisher/android-nixpkgs"; utils.url = "github:numtide/flake-utils";
android.inputs.nixpkgs.follows = "nixpkgs";
}; };
outputs = { self, nixpkgs, android }: outputs = { self, nixpkgs, utils }:
let utils.lib.eachDefaultSystem (system:
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-darwin" ]; let
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system); pkgs = import nixpkgs {
in inherit system;
{
devShells = forAllSystems # allow unfree Android packages.
(system: config.allowUnfree = true;
let # accept the Android SDK license.
pkgs = import nixpkgs { config.android_sdk.accept_license = true;
inherit system; };
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; in androidComposition.androidsdk;
[ in {
build-tools-31-0-0 default = with pkgs;
cmdline-tools-latest mkShell (rec {
platform-tools ANDROID_HOME = "${android-sdk}/libexec/android-sdk";
platforms-android-31 packages = [ android-sdk jdk clang ]
ndk-bundle ++ (if stdenv.isLinux then [
]);
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 [
vulkan-headers vulkan-headers
libxkbcommon libxkbcommon
wayland wayland
@@ -46,12 +42,13 @@
xorg.libXcursor xorg.libXcursor
xorg.libXfixes xorg.libXfixes
libGL libGL
pkgconfig pkg-config
] else [ ]); ] else
} // (if stdenv.isLinux then { [ ]);
LD_LIBRARY_PATH = "${vulkan-loader}/lib"; } // (if stdenv.isLinux then {
} else { })); LD_LIBRARY_PATH = "${vulkan-loader}/lib";
} } else
); { }));
}; };
});
} }
+126
View File
@@ -0,0 +1,126 @@
/*
Package font provides type describing font faces attributes.
*/
package font
import (
"github.com/go-text/typesetting/font"
)
// A FontFace is a Font and a matching Face.
type FontFace struct {
Font Font
Face Face
}
// Style is the font style.
type Style int
// Weight is a font weight, in CSS units subtracted 400 so the zero value
// is normal text weight.
type Weight int
// Font specify a particular typeface variant, style and weight.
type Font struct {
// Typeface specifies the name(s) of the the font faces to try. See [Typeface]
// for details.
Typeface Typeface
// Style specifies the kind of text style.
Style Style
// Weight is the text weight.
Weight Weight
}
// Face is an opaque handle to a typeface. The concrete implementation depends
// upon the kind of font and shaper in use.
type Face interface {
Face() *font.Face
}
// Typeface identifies a list of font families to attempt to use for displaying
// a string. The syntax is a comma-delimited list of family names. In order to
// allow for the remote possibility of needing to express a font family name
// containing a comma, name entries may be quoted using either single or double
// quotes. Within quotes, a literal quotation mark can be expressed by escaping
// it with `\`. A literal backslash may be expressed by escaping it with another
// `\`.
//
// Here's an example Typeface:
//
// Times New Roman, Georgia, serif
//
// This is equivalent to the above:
//
// "Times New Roman", 'Georgia', serif
//
// Here are some valid uses of escape sequences:
//
// "Contains a literal \" doublequote", 'Literal \' Singlequote', "\\ Literal backslash", '\\ another'
//
// This syntax has the happy side effect that most CSS "font-family" rules are
// valid Typefaces (without the trailing semicolon).
//
// Generic CSS font families are supported, and are automatically expanded to lists
// of known font families with a matching style. The supported generic families are:
//
// - fantasy
// - math
// - emoji
// - serif
// - sans-serif
// - cursive
// - monospace
type Typeface string
const (
Regular Style = iota
Italic
)
const (
Thin Weight = -300
ExtraLight Weight = -200
Light Weight = -100
Normal Weight = 0
Medium Weight = 100
SemiBold Weight = 200
Bold Weight = 300
ExtraBold Weight = 400
Black Weight = 500
)
func (s Style) String() string {
switch s {
case Regular:
return "Regular"
case Italic:
return "Italic"
default:
panic("invalid Style")
}
}
func (w Weight) String() string {
switch w {
case Thin:
return "Thin"
case ExtraLight:
return "ExtraLight"
case Light:
return "Light"
case Normal:
return "Normal"
case Medium:
return "Medium"
case SemiBold:
return "SemiBold"
case Bold:
return "Bold"
case ExtraBold:
return "ExtraBold"
case Black:
return "Black"
default:
panic("invalid Weight")
}
}
+38 -19
View File
@@ -24,29 +24,49 @@ import (
"golang.org/x/image/font/gofont/gosmallcaps" "golang.org/x/image/font/gofont/gosmallcaps"
"golang.org/x/image/font/gofont/gosmallcapsitalic" "golang.org/x/image/font/gofont/gosmallcapsitalic"
"gioui.org/font"
"gioui.org/font/opentype" "gioui.org/font/opentype"
"gioui.org/text"
) )
var ( var (
regOnce sync.Once
reg []font.FontFace
once sync.Once once sync.Once
collection []text.FontFace collection []font.FontFace
) )
func Collection() []text.FontFace { func loadRegular() {
regOnce.Do(func() {
faces, err := opentype.ParseCollection(goregular.TTF)
if err != nil {
panic(fmt.Errorf("failed to parse font: %v", err))
}
reg = faces
collection = append(collection, reg[0])
})
}
// Regular returns a collection of only the Go regular font face.
func Regular() []font.FontFace {
loadRegular()
return reg
}
// Regular returns a collection of all available Go font faces.
func Collection() []font.FontFace {
loadRegular()
once.Do(func() { once.Do(func() {
register(text.Font{}, goregular.TTF) register(goitalic.TTF)
register(text.Font{Style: text.Italic}, goitalic.TTF) register(gobold.TTF)
register(text.Font{Weight: text.Bold}, gobold.TTF) register(gobolditalic.TTF)
register(text.Font{Style: text.Italic, Weight: text.Bold}, gobolditalic.TTF) register(gomedium.TTF)
register(text.Font{Weight: text.Medium}, gomedium.TTF) register(gomediumitalic.TTF)
register(text.Font{Weight: text.Medium, Style: text.Italic}, gomediumitalic.TTF) register(gomono.TTF)
register(text.Font{Variant: "Mono"}, gomono.TTF) register(gomonobold.TTF)
register(text.Font{Variant: "Mono", Weight: text.Bold}, gomonobold.TTF) register(gomonobolditalic.TTF)
register(text.Font{Variant: "Mono", Weight: text.Bold, Style: text.Italic}, gomonobolditalic.TTF) register(gomonoitalic.TTF)
register(text.Font{Variant: "Mono", Style: text.Italic}, gomonoitalic.TTF) register(gosmallcaps.TTF)
register(text.Font{Variant: "Smallcaps"}, gosmallcaps.TTF) register(gosmallcapsitalic.TTF)
register(text.Font{Variant: "Smallcaps", Style: text.Italic}, gosmallcapsitalic.TTF)
// Ensure that any outside appends will not reuse the backing store. // Ensure that any outside appends will not reuse the backing store.
n := len(collection) n := len(collection)
collection = collection[:n:n] collection = collection[:n:n]
@@ -54,11 +74,10 @@ func Collection() []text.FontFace {
return collection return collection
} }
func register(fnt text.Font, ttf []byte) { func register(ttf []byte) {
face, err := opentype.Parse(ttf) faces, err := opentype.ParseCollection(ttf)
if err != nil { if err != nil {
panic(fmt.Errorf("failed to parse font: %v", err)) panic(fmt.Errorf("failed to parse font: %v", err))
} }
fnt.Typeface = "Go" collection = append(collection, faces[0])
collection = append(collection, text.FontFace{Font: fnt, Face: face})
} }
-530
View File
@@ -1,530 +0,0 @@
package internal
import (
"io"
"gioui.org/io/system"
"gioui.org/text"
"github.com/benoitkugler/textlayout/language"
"github.com/gioui/uax/segment"
"github.com/gioui/uax/uax14"
"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/math/fixed"
)
// computeGlyphClusters populates the Clusters field of a Layout.
// The order of the clusters is visual, meaning
// that the first cluster is the leftmost cluster displayed even when
// the cluster is part of RTL text.
func computeGlyphClusters(l *text.Layout) {
clusters := make([]text.GlyphCluster, 0, len(l.Glyphs)+1)
if len(l.Glyphs) < 1 {
if l.Runes.Count > 0 {
// Empty line corresponding to a newline character.
clusters = append(clusters, text.GlyphCluster{
Runes: text.Range{
Count: 1,
Offset: l.Runes.Offset,
},
})
}
l.Clusters = clusters
return
}
rtl := l.Direction == system.RTL
// Check for trailing whitespace characters and synthesize
// GlyphClusters to represent them.
lastGlyph := l.Glyphs[len(l.Glyphs)-1]
if rtl {
lastGlyph = l.Glyphs[0]
}
trailingNewline := lastGlyph.ClusterIndex+lastGlyph.RuneCount < l.Runes.Count+l.Runes.Offset
newlineCluster := text.GlyphCluster{
Runes: text.Range{
Count: 1,
Offset: l.Runes.Count + l.Runes.Offset - 1,
},
Glyphs: text.Range{
Offset: len(l.Glyphs),
},
}
var (
i int = 0
inc int = 1
runesProcessed int = 0
glyphsProcessed int = 0
)
if rtl {
i = len(l.Glyphs) - 1
inc = -inc
glyphsProcessed = len(l.Glyphs) - 1
newlineCluster.Glyphs.Offset = 0
}
// Construct clusters from the line's glyphs.
for ; i < len(l.Glyphs) && i >= 0; i += inc {
g := l.Glyphs[i]
xAdv := g.XAdvance * fixed.Int26_6(inc)
for k := 0; k < g.GlyphCount-1 && k < len(l.Glyphs); k++ {
i += inc
xAdv += l.Glyphs[i].XAdvance * fixed.Int26_6(inc)
}
startRune := runesProcessed
runeIncrement := g.RuneCount
startGlyph := glyphsProcessed
glyphIncrement := g.GlyphCount * inc
if rtl {
startGlyph = glyphsProcessed + glyphIncrement + 1
}
clusters = append(clusters, text.GlyphCluster{
Advance: xAdv,
Runes: text.Range{
Count: g.RuneCount,
Offset: startRune + l.Runes.Offset,
},
Glyphs: text.Range{
Count: g.GlyphCount,
Offset: startGlyph,
},
})
runesProcessed += runeIncrement
glyphsProcessed += glyphIncrement
}
// Insert synthetic clusters at the right edge of the line.
if trailingNewline {
clusters = append(clusters, newlineCluster)
}
l.Clusters = clusters
return
}
// langConfig describes the language and writing system of a body of text.
type langConfig struct {
// Language the text is written in.
language.Language
// Writing system used to represent the text.
language.Script
// Direction of the text, usually driven by the writing system.
di.Direction
}
// mapRunesToClusterIndices returns a slice. Each index within that slice corresponds
// to an index within the runes input slice. The value stored at that index is the
// index of the glyph at the start of the corresponding glyph cluster shaped by
// harfbuzz.
func mapRunesToClusterIndices(runes []rune, glyphs []shaping.Glyph) []int {
mapping := make([]int, len(runes))
glyphCursor := 0
if len(runes) == 0 {
return nil
}
// If the final cluster values are lower than the starting ones,
// the text is RTL.
rtl := len(glyphs) > 0 && glyphs[len(glyphs)-1].ClusterIndex < glyphs[0].ClusterIndex
if rtl {
glyphCursor = len(glyphs) - 1
}
for i := range runes {
for glyphCursor >= 0 && glyphCursor < len(glyphs) &&
((rtl && glyphs[glyphCursor].ClusterIndex <= i) ||
(!rtl && glyphs[glyphCursor].ClusterIndex < i)) {
if rtl {
glyphCursor--
} else {
glyphCursor++
}
}
if rtl {
glyphCursor++
} else if (glyphCursor >= 0 && glyphCursor < len(glyphs) &&
glyphs[glyphCursor].ClusterIndex > i) ||
(glyphCursor == len(glyphs) && len(glyphs) > 1) {
glyphCursor--
targetClusterIndex := glyphs[glyphCursor].ClusterIndex
for glyphCursor-1 >= 0 && glyphs[glyphCursor-1].ClusterIndex == targetClusterIndex {
glyphCursor--
}
}
if glyphCursor < 0 {
glyphCursor = 0
} else if glyphCursor >= len(glyphs) {
glyphCursor = len(glyphs) - 1
}
mapping[i] = glyphCursor
}
return mapping
}
// inclusiveGlyphRange returns the inclusive range of runes and glyphs matching
// the provided start and breakAfter rune positions.
// runeToGlyph must be a valid mapping from the rune representation to the
// glyph reprsentation produced by mapRunesToClusterIndices.
// numGlyphs is the number of glyphs in the output representing the runes
// under consideration.
func inclusiveGlyphRange(start, breakAfter int, runeToGlyph []int, numGlyphs int) (glyphStart, glyphEnd int) {
rtl := runeToGlyph[len(runeToGlyph)-1] < runeToGlyph[0]
runeStart := start
runeEnd := breakAfter
if rtl {
glyphStart = runeToGlyph[runeEnd]
if runeStart-1 >= 0 {
glyphEnd = runeToGlyph[runeStart-1] - 1
} else {
glyphEnd = numGlyphs - 1
}
} else {
glyphStart = runeToGlyph[runeStart]
if runeEnd+1 < len(runeToGlyph) {
glyphEnd = runeToGlyph[runeEnd+1] - 1
} else {
glyphEnd = numGlyphs - 1
}
}
return
}
// breakOption represets a location within the rune slice at which
// it may be safe to break a line of text.
type breakOption struct {
// breakAtRune is the index at which it is safe to break.
breakAtRune int
// penalty is the cost of breaking at this index. Negative
// penalties mean that the break is beneficial, and a penalty
// of uax14.PenaltyForMustBreak means a required break.
penalty int
}
// getBreakOptions returns a slice of line break candidates for the
// text in the provided slice.
func getBreakOptions(text []rune) []breakOption {
// Collect options for breaking the lines in a slice.
var options []breakOption
const adjust = -1
breaker := uax14.NewLineWrap()
segmenter := segment.NewSegmenter(breaker)
segmenter.InitFromSlice(text)
runeOffset := 0
brokeAtEnd := false
for segmenter.Next() {
penalty, _ := segmenter.Penalties()
// Determine the indices of the breaking runes in the runes
// slice. Would be nice if the API provided this.
currentSegment := segmenter.Runes()
runeOffset += len(currentSegment)
// Collect all break options.
options = append(options, breakOption{
penalty: penalty,
breakAtRune: runeOffset + adjust,
})
if options[len(options)-1].breakAtRune == len(text)-1 {
brokeAtEnd = true
}
}
if len(text) > 0 && !brokeAtEnd {
options = append(options, breakOption{
penalty: uax14.PenaltyForMustBreak,
breakAtRune: len(text) - 1,
})
}
return options
}
type Shaper func(shaping.Input) (shaping.Output, error)
// paragraph shapes a single paragraph of text, breaking it into multiple lines
// to fit within the provided maxWidth.
func paragraph(shaper Shaper, face font.Face, ppem fixed.Int26_6, maxWidth int, lc langConfig, paragraph []rune) ([]output, error) {
// TODO: handle splitting bidi text here
// Shape the text.
input := toInput(face, ppem, lc, paragraph)
out, err := shaper(input)
if err != nil {
return nil, err
}
// Get a mapping from input runes to output glyphs.
runeToGlyph := mapRunesToClusterIndices(paragraph, out.Glyphs)
// Fetch line break candidates.
breaks := getBreakOptions(paragraph)
return lineWrap(out, input.Direction, paragraph, runeToGlyph, breaks, maxWidth), nil
}
// shouldKeepSegmentOnLine decides whether the segment of text from the current
// end of the line to the provided breakOption should be kept on the current
// line. It should be called successively with each available breakOption,
// and the line should be broken (without keeping the current segment)
// whenever it returns false.
//
// The parameters require some explanation:
// out - the shaping.Output that is being line-broken.
// runeToGlyph - a mapping where accessing the slice at the index of a rune
// int out will yield the index of the first glyph corresponding to that rune.
// lineStartRune - the index of the first rune in the line.
// b - the line break candidate under consideration.
// curLineWidth - the amount of space total in the current line.
// curLineUsed - the amount of space in the current line that is already used.
// nextLineWidth - the amount of space available on the next line.
//
// This function returns both a valid shaping.Output broken at b and a boolean
// indicating whether the returned output should be used.
func shouldKeepSegmentOnLine(out shaping.Output, runeToGlyph []int, lineStartRune int, b breakOption, curLineWidth, curLineUsed, nextLineWidth int) (candidateLine shaping.Output, keep bool) {
// Convert the break target to an inclusive index.
glyphStart, glyphEnd := inclusiveGlyphRange(lineStartRune, b.breakAtRune, runeToGlyph, len(out.Glyphs))
// Construct a line out of the inclusive glyph range.
candidateLine = out
candidateLine.Glyphs = candidateLine.Glyphs[glyphStart : glyphEnd+1]
candidateLine.RecomputeAdvance()
candidateAdvance := candidateLine.Advance.Ceil()
if candidateAdvance > curLineWidth && candidateAdvance-curLineUsed <= nextLineWidth {
// If it fits on the next line, put it there.
return candidateLine, false
}
return candidateLine, true
}
// lineWrap wraps the shaped glyphs of a paragraph to a particular max width.
func lineWrap(out shaping.Output, dir di.Direction, paragraph []rune, runeToGlyph []int, breaks []breakOption, maxWidth int) []output {
var outputs []output
if len(breaks) == 0 {
// Pass empty lines through as empty.
outputs = append(outputs, output{
Shaped: out,
RuneRange: text.Range{
Count: len(paragraph),
},
})
return outputs
}
for i := 0; i < len(breaks); i++ {
b := breaks[i]
if b.breakAtRune+1 < len(runeToGlyph) {
// Check if this break is valid.
gIdx := runeToGlyph[b.breakAtRune]
g2Idx := runeToGlyph[b.breakAtRune+1]
cIdx := out.Glyphs[gIdx].ClusterIndex
c2Idx := out.Glyphs[g2Idx].ClusterIndex
if cIdx == c2Idx {
// This break is within a harfbuzz cluster, and is
// therefore invalid.
copy(breaks[i:], breaks[i+1:])
breaks = breaks[:len(breaks)-1]
i--
}
}
}
start := 0
runesProcessed := 0
for i := 0; i < len(breaks); i++ {
b := breaks[i]
// Always keep the first segment on a line.
good, _ := shouldKeepSegmentOnLine(out, runeToGlyph, start, b, maxWidth, 0, maxWidth)
end := b.breakAtRune
innerLoop:
for k := i + 1; k < len(breaks); k++ {
bb := breaks[k]
candidate, ok := shouldKeepSegmentOnLine(out, runeToGlyph, start, bb, maxWidth, good.Advance.Ceil(), maxWidth)
if ok {
// Use this new, longer segment.
good = candidate
end = bb.breakAtRune
i++
} else {
break innerLoop
}
}
numRunes := end - start + 1
outputs = append(outputs, output{
Shaped: good,
RuneRange: text.Range{
Count: numRunes,
Offset: runesProcessed,
},
})
runesProcessed += numRunes
start = end + 1
}
return outputs
}
// output is a run of shaped text with metadata about its position
// within a text document.
type output struct {
Shaped shaping.Output
RuneRange text.Range
}
func toSystemDirection(d di.Direction) system.TextDirection {
switch d {
case di.DirectionLTR:
return system.LTR
case di.DirectionRTL:
return system.RTL
}
return system.LTR
}
// toGioGlyphs converts text shaper glyphs into the minimal representation
// that Gio needs.
func toGioGlyphs(in []shaping.Glyph) []text.Glyph {
out := make([]text.Glyph, 0, len(in))
for _, g := range in {
out = append(out, text.Glyph{
ID: g.GlyphID,
ClusterIndex: g.ClusterIndex,
RuneCount: g.RuneCount,
GlyphCount: g.GlyphCount,
XAdvance: g.XAdvance,
YAdvance: g.YAdvance,
XOffset: g.XOffset,
YOffset: g.YOffset,
})
}
return out
}
// ToLine converts the output into a text.Line
func (o output) ToLine() text.Line {
advances := make([]fixed.Int26_6, 0, len(o.Shaped.Glyphs))
for _, glyph := range o.Shaped.Glyphs {
advances = append(advances, glyph.XAdvance)
}
layout := text.Layout{
Glyphs: toGioGlyphs(o.Shaped.Glyphs),
Runes: o.RuneRange,
Direction: toSystemDirection(o.Shaped.Direction),
}
return text.Line{
Layout: layout,
Bounds: fixed.Rectangle26_6{
Min: fixed.Point26_6{
Y: -o.Shaped.LineBounds.Ascent,
},
Max: fixed.Point26_6{
X: o.Shaped.Advance,
Y: -o.Shaped.LineBounds.Ascent + o.Shaped.LineBounds.LineHeight(),
},
},
Width: o.Shaped.Advance,
Ascent: o.Shaped.LineBounds.Ascent,
Descent: -o.Shaped.LineBounds.Descent + o.Shaped.LineBounds.Gap,
}
}
func mapDirection(d system.TextDirection) di.Direction {
switch d {
case system.LTR:
return di.DirectionLTR
case system.RTL:
return di.DirectionRTL
}
return di.DirectionLTR
}
// Document shapes text using the given font, ppem, maximum line width, language,
// and sequence of runes. It returns a slice of lines corresponding to the txt,
// broken to fit within maxWidth and on paragraph boundaries.
func Document(shaper Shaper, face font.Face, ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) []text.Line {
var (
outputs []text.Line
startByte int
startRune int
paragraphText []rune
done bool
langs = make(map[language.Script]int)
)
for !done {
var (
bytes int
runes int
)
newlineAdjust := 0
paragraphLoop:
for r, sz, re := txt.ReadRune(); !done; r, sz, re = txt.ReadRune() {
if re != nil {
done = true
continue
}
paragraphText = append(paragraphText, r)
script := language.LookupScript(r)
if _, ok := langs[script]; ok {
langs[script]++
} else {
langs[script] = 1
}
bytes += sz
runes++
if r == '\n' {
newlineAdjust = 1
break paragraphLoop
}
}
var (
primary language.Script
primaryTotal int
)
for script, total := range langs {
if total > primaryTotal {
primary = script
primaryTotal = total
}
}
if lc.Language == "" {
lc.Language = "EN"
}
lcfg := langConfig{
Language: language.NewLanguage(lc.Language),
Script: primary,
Direction: mapDirection(lc.Direction),
}
lines, _ := paragraph(shaper, face, ppem, maxWidth, lcfg, paragraphText[:len(paragraphText)-newlineAdjust])
for i := range lines {
// Update the offsets of each paragraph to be correct within the
// whole document.
lines[i].RuneRange.Offset += startRune
// Update the cluster values to be rune indices within the entire
// document.
for k := range lines[i].Shaped.Glyphs {
lines[i].Shaped.Glyphs[k].ClusterIndex += startRune
}
outputs = append(outputs, lines[i].ToLine())
}
// If there was a trailing newline update the byte counts to include
// it on the last line of the paragraph.
if newlineAdjust > 0 {
outputs[len(outputs)-1].Layout.Runes.Count += newlineAdjust
}
paragraphText = paragraphText[:0]
startByte += bytes
startRune += runes
}
for i := range outputs {
computeGlyphClusters(&outputs[i].Layout)
}
return outputs
}
// toInput converts its parameters into a shaping.Input.
func toInput(face font.Face, ppem fixed.Int26_6, lc langConfig, runes []rune) shaping.Input {
var input shaping.Input
input.Direction = lc.Direction
input.Text = runes
input.Size = ppem
input.Face = face
input.Language = lc.Language
input.Script = lc.Script
input.RunStart = 0
input.RunEnd = len(runes)
return input
}
File diff suppressed because it is too large Load Diff
+169 -117
View File
@@ -2,137 +2,189 @@
// Package opentype implements text layout and shaping for OpenType // Package opentype implements text layout and shaping for OpenType
// files. // files.
//
// NOTE: the OpenType specification allows for fonts to include bitmap images
// in a variety of formats. In the interest of small binary sizes, the opentype
// package only automatically imports the PNG image decoder. If you have a font
// with glyphs in JPEG or TIFF formats, register those decoders with the image
// package in order to ensure those glyphs are visible in text.
package opentype package opentype
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"image" _ "image/png"
"io"
"github.com/benoitkugler/textlayout/fonts" giofont "gioui.org/font"
"github.com/benoitkugler/textlayout/fonts/truetype" fontapi "github.com/go-text/typesetting/font"
"github.com/benoitkugler/textlayout/harfbuzz" "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/shaping"
"golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"gioui.org/f32"
"gioui.org/font/opentype/internal"
"gioui.org/io/system"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/text"
) )
// Font implements the text.Shaper interface using a rich text // Face is a thread-safe representation of a loaded font. For efficiency, applications
// shaping engine. // should construct a face for any given font file once, reusing it across different
type Font struct { // text shapers.
font *truetype.Font type Face struct {
face *fontapi.Font
font giofont.Font
} }
// Parse constructs a Font from source bytes. // Parse constructs a Face from source bytes.
func Parse(src []byte) (*Font, error) { func Parse(src []byte) (Face, error) {
face, err := truetype.Parse(bytes.NewReader(src)) ld, err := opentype.NewLoader(bytes.NewReader(src))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed parsing truetype font: %w", err) return Face{}, err
} }
return &Font{ font, md, err := parseLoader(ld)
font: face, if err != nil {
return Face{}, fmt.Errorf("failed parsing truetype font: %w", err)
}
return Face{
face: font,
font: md,
}, nil }, nil
} }
func (f *Font) Layout(ppem fixed.Int26_6, maxWidth int, lc system.Locale, txt io.RuneReader) ([]text.Line, error) { // ParseCollection parse an Opentype font file, with support for collections.
return internal.Document(shaping.Shape, f.font, ppem, maxWidth, lc, txt), nil // Single font files are supported, returning a slice with length 1.
} // The returned fonts are automatically wrapped in a text.FontFace with
// inferred font font.
func (f *Font) Shape(ppem fixed.Int26_6, str text.Layout) clip.PathSpec { // BUG(whereswaldon): the only Variant that can be detected automatically is
return textPath(ppem, f, str) // "Mono".
} func ParseCollection(src []byte) ([]giofont.FontFace, error) {
lds, err := opentype.NewLoaders(bytes.NewReader(src))
func (f *Font) Metrics(ppem fixed.Int26_6) font.Metrics { if err != nil {
metrics := font.Metrics{} return nil, err
font := harfbuzz.NewFont(f.font) }
font.XScale = int32(ppem.Ceil()) << 6 out := make([]giofont.FontFace, len(lds))
font.YScale = font.XScale for i, ld := range lds {
// Use any horizontal direction. face, md, err := parseLoader(ld)
fontExtents := font.ExtentsForDirection(harfbuzz.LeftToRight) if err != nil {
ascender := fixed.I(int(fontExtents.Ascender * 64)) return nil, fmt.Errorf("reading font %d of collection: %s", i, err)
descender := fixed.I(int(fontExtents.Descender * 64)) }
gap := fixed.I(int(fontExtents.LineGap * 64)) ff := Face{
metrics.Height = ascender + descender + gap face: face,
metrics.Ascent = ascender font: md,
metrics.Descent = descender }
// These three are not readily available. out[i] = giofont.FontFace{
// TODO(whereswaldon): figure out how to get these values. Face: ff,
metrics.XHeight = ascender Font: ff.Font(),
metrics.CapHeight = ascender }
metrics.CaretSlope = image.Pt(0, 1) }
return metrics return out, nil
} }
func textPath(ppem fixed.Int26_6, font *Font, str text.Layout) clip.PathSpec { func DescriptionToFont(md fontapi.Description) giofont.Font {
var lastPos f32.Point return giofont.Font{
var builder clip.Path Typeface: giofont.Typeface(md.Family),
ops := new(op.Ops) Style: gioStyle(md.Aspect.Style),
var x fixed.Int26_6 Weight: gioWeight(md.Aspect.Weight),
builder.Begin(ops) }
rune := 0 }
ppemInt := ppem.Round()
ppem16 := uint16(ppemInt) func FontToDescription(font giofont.Font) fontapi.Description {
scaleFactor := float32(ppemInt) / float32(font.font.Upem()) return fontapi.Description{
for _, g := range str.Glyphs { Family: string(font.Typeface),
advance := g.XAdvance Aspect: fontapi.Aspect{
outline, ok := font.font.GlyphData(g.ID, ppem16, ppem16).(fonts.GlyphOutline) Style: mdStyle(font.Style),
if !ok { Weight: mdWeight(font.Weight),
continue },
} }
// Move to glyph position. }
pos := f32.Point{
X: float32(x)/64 - float32(g.XOffset)/64, // parseLoader parses the contents of the loader into a face and its font.
Y: -float32(g.YOffset) / 64, func parseLoader(ld *opentype.Loader) (*fontapi.Font, giofont.Font, error) {
} ft, err := fontapi.NewFont(ld)
builder.Move(pos.Sub(lastPos)) if err != nil {
lastPos = pos return nil, giofont.Font{}, err
var lastArg f32.Point }
data := DescriptionToFont(ft.Describe())
// Convert sfnt.Segments to relative segments. return ft, data, nil
for _, fseg := range outline.Segments { }
nargs := 1
switch fseg.Op { // Face returns a thread-unsafe wrapper for this Face suitable for use by a single shaper.
case fonts.SegmentOpQuadTo: // Face many be invoked any number of times and is safe so long as each return value is
nargs = 2 // only used by one goroutine.
case fonts.SegmentOpCubeTo: func (f Face) Face() *fontapi.Face {
nargs = 3 return fontapi.NewFace(f.face)
} }
var args [3]f32.Point
for i := 0; i < nargs; i++ { // FontFace returns a text.Font with populated font metadata for the
a := f32.Point{ // font.
X: fseg.Args[i].X * scaleFactor, // BUG(whereswaldon): the only Variant that can be detected automatically is
Y: -fseg.Args[i].Y * scaleFactor, // "Mono".
} func (f Face) Font() giofont.Font {
args[i] = a.Sub(lastArg) return f.font
if i == nargs-1 { }
lastArg = a
} func gioStyle(s fontapi.Style) giofont.Style {
} switch s {
switch fseg.Op { case fontapi.StyleItalic:
case fonts.SegmentOpMoveTo: return giofont.Italic
builder.Move(args[0]) case fontapi.StyleNormal:
case fonts.SegmentOpLineTo: fallthrough
builder.Line(args[0]) default:
case fonts.SegmentOpQuadTo: return giofont.Regular
builder.Quad(args[0], args[1]) }
case fonts.SegmentOpCubeTo: }
builder.Cube(args[0], args[1], args[2])
default: func mdStyle(g giofont.Style) fontapi.Style {
panic("unsupported segment op") switch g {
} case giofont.Italic:
} return fontapi.StyleItalic
lastPos = lastPos.Add(lastArg) case giofont.Regular:
x += advance fallthrough
rune++ default:
return fontapi.StyleNormal
}
}
func gioWeight(w fontapi.Weight) giofont.Weight {
switch w {
case fontapi.WeightThin:
return giofont.Thin
case fontapi.WeightExtraLight:
return giofont.ExtraLight
case fontapi.WeightLight:
return giofont.Light
case fontapi.WeightNormal:
return giofont.Normal
case fontapi.WeightMedium:
return giofont.Medium
case fontapi.WeightSemibold:
return giofont.SemiBold
case fontapi.WeightBold:
return giofont.Bold
case fontapi.WeightExtraBold:
return giofont.ExtraBold
case fontapi.WeightBlack:
return giofont.Black
default:
return giofont.Normal
}
}
func mdWeight(g giofont.Weight) fontapi.Weight {
switch g {
case giofont.Thin:
return fontapi.WeightThin
case giofont.ExtraLight:
return fontapi.WeightExtraLight
case giofont.Light:
return fontapi.WeightLight
case giofont.Normal:
return fontapi.WeightNormal
case giofont.Medium:
return fontapi.WeightMedium
case giofont.SemiBold:
return fontapi.WeightSemibold
case giofont.Bold:
return fontapi.WeightBold
case giofont.ExtraBold:
return fontapi.WeightExtraBold
case giofont.Black:
return fontapi.WeightBlack
default:
return fontapi.WeightNormal
} }
return builder.End()
} }
-151
View File
@@ -1,151 +0,0 @@
package opentype
import (
"bytes"
"compress/gzip"
"encoding/binary"
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/math/fixed"
"gioui.org/internal/ops"
"gioui.org/io/system"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/text"
)
var english = system.Locale{
Language: "EN",
Direction: system.LTR,
}
func TestEmptyString(t *testing.T) {
face, err := Parse(goregular.TTF)
if err != nil {
t.Fatal(err)
}
ppem := fixed.I(200)
lines, err := face.Layout(ppem, 2000, english, strings.NewReader(""))
if err != nil {
t.Fatal(err)
}
if len(lines) == 0 {
t.Fatalf("Layout returned no lines for empty string; expected 1")
}
l := lines[0]
exp := fixed.Rectangle26_6{
Min: fixed.Point26_6{
Y: fixed.Int26_6(-12094),
},
Max: fixed.Point26_6{
Y: fixed.Int26_6(2700),
},
}
if got := l.Bounds; got != exp {
t.Errorf("got bounds %+v for empty string; expected %+v", got, exp)
}
}
func decompressFontFile(name string) (*Font, []byte, error) {
f, err := os.Open(name)
if err != nil {
return nil, nil, fmt.Errorf("could not open file for reading: %s: %v", name, err)
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
return nil, nil, fmt.Errorf("font file contains invalid gzip data: %v", err)
}
src, err := ioutil.ReadAll(gz)
if err != nil {
return nil, nil, fmt.Errorf("failed to decompress font file: %v", err)
}
fnt, err := Parse(src)
if err != nil {
return nil, nil, fmt.Errorf("file did not contain a valid font: %v", err)
}
return fnt, src, nil
}
// mergeFonts produces a trivial OpenType Collection (OTC) file for two source fonts.
// It makes many assumptions and is not meant for general use.
// For file format details, see https://docs.microsoft.com/en-us/typography/opentype/spec/otff
// For a robust tool to generate these files, see https://pypi.org/project/afdko/
func mergeFonts(ttf1, ttf2 []byte) []byte {
// Locations to place the two embedded fonts. All of the offsets to the fonts' internal tables will need to be
// shifted from the start of the file by the appropriate amount, and then everything will work as expected.
offset1 := uint32(20) // Length of OpenType collection headers
offset2 := offset1 + uint32(len(ttf1))
var buf bytes.Buffer
_, _ = buf.Write([]byte("ttcf\x00\x01\x00\x00\x00\x00\x00\x02"))
_ = binary.Write(&buf, binary.BigEndian, offset1)
_ = binary.Write(&buf, binary.BigEndian, offset2)
// Inline function to copy a font into the collection verbatim, except for adding an offset to all of the font's
// table positions.
copyOffsetTTF := func(ttf []byte, offset uint32) {
_, _ = buf.Write(ttf[:12])
numTables := binary.BigEndian.Uint16(ttf[4:6])
for i := uint16(0); i < numTables; i++ {
p := 12 + 16*i
_, _ = buf.Write(ttf[p : p+8])
tblLoc := binary.BigEndian.Uint32(ttf[p+8:p+12]) + offset
_ = binary.Write(&buf, binary.BigEndian, tblLoc)
_, _ = buf.Write(ttf[p+12 : p+16])
}
_, _ = buf.Write(ttf[12+16*numTables:])
}
copyOffsetTTF(ttf1, offset1)
copyOffsetTTF(ttf2, offset2)
return buf.Bytes()
}
// shapeRune uses a given Face to shape exactly one rune at a fixed size, then returns the resulting shape data.
func shapeRune(f text.Face, r rune) (clip.PathSpec, error) {
ppem := fixed.I(200)
lines, err := f.Layout(ppem, 2000, english, strings.NewReader(string(r)))
if err != nil {
return clip.PathSpec{}, err
}
if len(lines) != 1 {
return clip.PathSpec{}, fmt.Errorf("unexpected rendering for \"U+%08X\": got %d lines (expected: 1)", r, len(lines))
}
return f.Shape(ppem, lines[0].Layout), nil
}
// areShapesEqual returns true iff both given text shapes are produced with identical operations.
func areShapesEqual(shape1, shape2 clip.PathSpec) bool {
var ops1, ops2 op.Ops
clip.Outline{Path: shape1}.Op().Push(&ops1).Pop()
clip.Outline{Path: shape2}.Op().Push(&ops2).Pop()
var r1, r2 ops.Reader
r1.Reset(&ops1.Internal)
r2.Reset(&ops2.Internal)
for {
encOp1, ok1 := r1.Decode()
encOp2, ok2 := r2.Decode()
if ok1 != ok2 {
return false
}
if !ok1 {
break
}
if len(encOp1.Refs) > 0 || len(encOp2.Refs) > 0 {
panic("unexpected ops with refs in font shaping test")
}
if !bytes.Equal(encOp1.Data, encOp2.Data) {
return false
}
}
return true
}
Binary file not shown.
Binary file not shown.
+118 -110
View File
@@ -18,6 +18,7 @@ import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/fling" "gioui.org/internal/fling"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/op" "gioui.org/op"
@@ -37,31 +38,31 @@ type Hover struct {
// Add the gesture to detect hovering over the current pointer area. // Add the gesture to detect hovering over the current pointer area.
func (h *Hover) Add(ops *op.Ops) { func (h *Hover) Add(ops *op.Ops) {
pointer.InputOp{ event.Op(ops, h)
Tag: h,
Types: pointer.Enter | pointer.Leave,
}.Add(ops)
} }
// Hovered returns whether a pointer is inside the area. // Update state and report whether a pointer is inside the area.
func (h *Hover) Hovered(q event.Queue) bool { func (h *Hover) Update(q input.Source) bool {
for _, ev := range q.Events(h) { for {
ev, ok := q.Event(pointer.Filter{
Target: h,
Kinds: pointer.Enter | pointer.Leave | pointer.Cancel,
})
if !ok {
break
}
e, ok := ev.(pointer.Event) e, ok := ev.(pointer.Event)
if !ok { if !ok {
continue continue
} }
switch e.Type { switch e.Kind {
case pointer.Leave, pointer.Cancel: case pointer.Leave, pointer.Cancel:
if h.entered && h.pid == e.PointerID { if h.entered && h.pid == e.PointerID {
h.entered = false h.entered = false
} }
case pointer.Enter: case pointer.Enter:
if !h.entered { h.pid = e.PointerID
h.pid = e.PointerID h.entered = true
}
if h.pid == e.PointerID {
h.entered = true
}
} }
} }
return h.entered return h.entered
@@ -87,11 +88,11 @@ type Click struct {
} }
// ClickEvent represent a click action, either a // ClickEvent represent a click action, either a
// TypePress for the beginning of a click or a // KindPress for the beginning of a click or a
// TypeClick for a completed click. // KindClick for a completed click.
type ClickEvent struct { type ClickEvent struct {
Type ClickType Kind ClickKind
Position f32.Point Position image.Point
Source pointer.Source Source pointer.Source
Modifiers key.Modifiers Modifiers key.Modifiers
// NumClicks records successive clicks occurring // NumClicks records successive clicks occurring
@@ -99,7 +100,7 @@ type ClickEvent struct {
NumClicks int NumClicks int
} }
type ClickType uint8 type ClickKind uint8
// Drag detects drag gestures in the form of pointer.Drag events. // Drag detects drag gestures in the form of pointer.Drag events.
type Drag struct { type Drag struct {
@@ -107,7 +108,6 @@ type Drag struct {
pressed bool pressed bool
pid pointer.ID pid pointer.ID
start f32.Point start f32.Point
grab bool
} }
// Scroll detects scroll gestures and reduces them to // Scroll detects scroll gestures and reduces them to
@@ -115,11 +115,9 @@ type Drag struct {
// movements as well as drag and fling touch gestures. // movements as well as drag and fling touch gestures.
type Scroll struct { type Scroll struct {
dragging bool dragging bool
axis Axis
estimator fling.Extrapolation estimator fling.Extrapolation
flinger fling.Animation flinger fling.Animation
pid pointer.ID pid pointer.ID
grab bool
last int last int
// Leftover scroll. // Leftover scroll.
scroll float32 scroll float32
@@ -136,15 +134,15 @@ const (
) )
const ( const (
// TypePress is reported for the first pointer // KindPress is reported for the first pointer
// press. // press.
TypePress ClickType = iota KindPress ClickKind = iota
// TypeClick is reported when a click action // KindClick is reported when a click action
// is complete. // is complete.
TypeClick KindClick
// TypeCancel is reported when the gesture is // KindCancel is reported when the gesture is
// cancelled. // cancelled.
TypeCancel KindCancel
) )
const ( const (
@@ -157,14 +155,11 @@ const (
StateFlinging StateFlinging
) )
var touchSlop = unit.Dp(3) const touchSlop = unit.Dp(3)
// Add the handler to the operation list to receive click events. // Add the handler to the operation list to receive click events.
func (c *Click) Add(ops *op.Ops) { func (c *Click) Add(ops *op.Ops) {
pointer.InputOp{ event.Op(ops, c)
Tag: c,
Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
}.Add(ops)
} }
// Hovered returns whether a pointer is inside the area. // Hovered returns whether a pointer is inside the area.
@@ -177,30 +172,36 @@ func (c *Click) Pressed() bool {
return c.pressed return c.pressed
} }
// Events returns the next click events, if any. // Update state and return the next click events, if any.
func (c *Click) Events(q event.Queue) []ClickEvent { func (c *Click) Update(q input.Source) (ClickEvent, bool) {
var events []ClickEvent for {
for _, evt := range q.Events(c) { 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) e, ok := evt.(pointer.Event)
if !ok { if !ok {
continue continue
} }
switch e.Type { switch e.Kind {
case pointer.Release: case pointer.Release:
if !c.pressed || c.pid != e.PointerID { if !c.pressed || c.pid != e.PointerID {
break break
} }
c.pressed = false c.pressed = false
if !c.entered || c.hovered { if !c.entered || c.hovered {
if e.Time-c.clickedAt < doubleClickDuration { return ClickEvent{
c.clicks++ Kind: KindClick,
} else { Position: e.Position.Round(),
c.clicks = 1 Source: e.Source,
} Modifiers: e.Modifiers,
c.clickedAt = e.Time NumClicks: c.clicks,
events = append(events, ClickEvent{Type: TypeClick, Position: e.Position, Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}) }, true
} else { } else {
events = append(events, ClickEvent{Type: TypeCancel}) return ClickEvent{Kind: KindCancel}, true
} }
case pointer.Cancel: case pointer.Cancel:
wasPressed := c.pressed wasPressed := c.pressed
@@ -208,7 +209,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
c.hovered = false c.hovered = false
c.entered = false c.entered = false
if wasPressed { if wasPressed {
events = append(events, ClickEvent{Type: TypeCancel}) return ClickEvent{Kind: KindCancel}, true
} }
case pointer.Press: case pointer.Press:
if c.pressed { if c.pressed {
@@ -217,14 +218,15 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary { if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary {
break break
} }
if !c.hovered { c.pid = e.PointerID
c.pid = e.PointerID
}
if c.pid != e.PointerID {
break
}
c.pressed = true c.pressed = true
events = append(events, ClickEvent{Type: TypePress, Position: e.Position, Source: e.Source, Modifiers: e.Modifiers}) if e.Time-c.clickedAt < doubleClickDuration {
c.clicks++
} else {
c.clicks = 1
}
c.clickedAt = e.Time
return ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}, true
case pointer.Leave: case pointer.Leave:
if !c.pressed { if !c.pressed {
c.pid = e.PointerID c.pid = e.PointerID
@@ -242,25 +244,16 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
} }
} }
} }
return events return ClickEvent{}, false
} }
func (ClickEvent) ImplementsEvent() {} func (ClickEvent) ImplementsEvent() {}
// Add the handler to the operation list to receive scroll events. // Add the handler to the operation list to receive scroll events.
// The bounds variable refers to the scrolling boundaries // The bounds variable refers to the scrolling boundaries
// as defined in io/pointer.InputOp. // as defined in [pointer.Filter].
func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) { func (s *Scroll) Add(ops *op.Ops) {
oph := pointer.InputOp{ event.Op(ops, s)
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)
}
} }
// Stop any remaining fling movement. // Stop any remaining fling movement.
@@ -268,20 +261,25 @@ func (s *Scroll) Stop() {
s.flinger = fling.Animation{} s.flinger = fling.Animation{}
} }
// Scroll detects the scrolling distance from the available events and // Update state and report the scroll distance along axis.
// ongoing fling gestures. func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, scrollx, scrolly pointer.ScrollRange) int {
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
}
total := 0 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) e, ok := evt.(pointer.Event)
if !ok { if !ok {
continue continue
} }
switch e.Type { switch e.Kind {
case pointer.Press: case pointer.Press:
if s.dragging { if s.dragging {
break break
@@ -293,7 +291,7 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
} }
s.Stop() s.Stop()
s.estimator = fling.Extrapolation{} s.estimator = fling.Extrapolation{}
v := s.val(e.Position) v := s.val(axis, e.Position)
s.last = int(math.Round(float64(v))) s.last = int(math.Round(float64(v)))
s.estimator.Sample(e.Time, v) s.estimator.Sample(e.Time, v)
s.dragging = true s.dragging = true
@@ -303,19 +301,20 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
break break
} }
fling := s.estimator.Estimate() fling := s.estimator.Estimate()
if slop, d := float32(cfg.Px(touchSlop)), fling.Distance; d < -slop || d > slop { if slop, d := float32(cfg.Dp(touchSlop)), fling.Distance; d < -slop || d > slop {
s.flinger.Start(cfg, t, fling.Velocity) s.flinger.Start(cfg, t, fling.Velocity)
} }
fallthrough fallthrough
case pointer.Cancel: case pointer.Cancel:
s.dragging = false s.dragging = false
s.grab = false
case pointer.Scroll: case pointer.Scroll:
switch s.axis { switch axis {
case Horizontal: case Horizontal:
s.scroll += e.Scroll.X s.scroll += e.Scroll.X
case Vertical: case Vertical:
s.scroll += e.Scroll.Y s.scroll += e.Scroll.Y
case Both:
s.scroll += e.Scroll.X + e.Scroll.Y
} }
iscroll := int(s.scroll) iscroll := int(s.scroll)
s.scroll -= float32(iscroll) 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 { if !s.dragging || s.pid != e.PointerID {
continue continue
} }
val := s.val(e.Position) val := s.val(axis, e.Position)
s.estimator.Sample(e.Time, val) s.estimator.Sample(e.Time, val)
v := int(math.Round(float64(val))) v := int(math.Round(float64(val)))
dist := s.last - v dist := s.last - v
if e.Priority < pointer.Grabbed { if e.Priority < pointer.Grabbed {
slop := cfg.Px(touchSlop) slop := cfg.Dp(touchSlop)
if dist := dist; dist >= slop || -slop >= dist { if dist := dist; dist >= slop || -slop >= dist {
s.grab = true q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID})
} }
} else { } else {
s.last = v 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) total += s.flinger.Tick(t)
if s.flinger.Active() {
q.Execute(op.InvalidateCmd{})
}
return total return total
} }
func (s *Scroll) val(p f32.Point) float32 { func (s *Scroll) val(axis Axis, p f32.Point) float32 {
if s.axis == Horizontal { switch axis {
case Horizontal:
return p.X return p.X
} else { case Vertical:
return p.Y 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. // Add the handler to the operation list to receive drag events.
func (d *Drag) Add(ops *op.Ops) { func (d *Drag) Add(ops *op.Ops) {
pointer.InputOp{ event.Op(ops, d)
Tag: d,
Grab: d.grab,
Types: pointer.Press | pointer.Drag | pointer.Release,
}.Add(ops)
} }
// Events returns the next drag events, if any. // Update state and return the next drag event, if any.
func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event { func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event, bool) {
var events []pointer.Event for {
for _, e := range q.Events(d) { ev, ok := q.Event(pointer.Filter{
e, ok := e.(pointer.Event) Target: d,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
})
if !ok {
break
}
e, ok := ev.(pointer.Event)
if !ok { if !ok {
continue continue
} }
switch e.Type { switch e.Kind {
case pointer.Press: case pointer.Press:
if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) { if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
continue continue
@@ -407,9 +416,9 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
} }
if e.Priority < pointer.Grabbed { if e.Priority < pointer.Grabbed {
diff := e.Position.Sub(d.start) diff := e.Position.Sub(d.start)
slop := cfg.Px(touchSlop) slop := cfg.Dp(touchSlop)
if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) { 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: case pointer.Release, pointer.Cancel:
@@ -418,13 +427,12 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
continue continue
} }
d.dragging = false 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. // 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 { switch ct {
case TypePress: case KindPress:
return "TypePress" return "KindPress"
case TypeClick: case KindClick:
return "TypeClick" return "KindClick"
case TypeCancel: case KindCancel:
return "TypeCancel" return "KindCancel"
default: default:
panic("invalid ClickType") panic("invalid ClickKind")
} }
} }
+95 -22
View File
@@ -9,8 +9,8 @@ import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
) )
@@ -22,20 +22,21 @@ func TestHover(t *testing.T) {
stack := clip.Rect(rect).Push(ops) stack := clip.Rect(rect).Push(ops)
h.Add(ops) h.Add(ops)
stack.Pop() stack.Pop()
r := new(router.Router) r := new(input.Router)
h.Update(r.Source())
r.Frame(ops) r.Frame(ops)
r.Queue( 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") t.Fatal("expected hovered")
} }
r.Queue( 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") t.Fatal("expected not hovered")
} }
} }
@@ -71,12 +72,21 @@ func TestMouseClicks(t *testing.T) {
var ops op.Ops var ops op.Ops
click.Add(&ops) click.Add(&ops)
var r router.Router var r input.Router
click.Update(r.Source())
r.Frame(&ops) r.Frame(&ops)
r.Queue(tc.events...) r.Queue(tc.events...)
events := click.Events(&r) var clicks []ClickEvent
clicks := filterMouseClicks(events) 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 { if got, want := len(clicks), len(tc.clicks); got != want {
t.Fatalf("got %d mouse clicks, expected %d", got, want) t.Fatalf("got %d mouse clicks, expected %d", got, want)
} }
@@ -90,28 +100,91 @@ 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 { func mouseClickEvents(times ...time.Duration) []event.Event {
press := pointer.Event{ press := pointer.Event{
Type: pointer.Press, Kind: pointer.Press,
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
} }
events := make([]event.Event, 0, 2*len(times)) events := make([]event.Event, 0, 2*len(times))
for _, t := range times { for _, t := range times {
press := press
press.Time = t
release := press release := press
release.Type = pointer.Release release.Kind = pointer.Release
release.Time = t
events = append(events, press, release) events = append(events, press, release)
} }
return events return events
} }
func filterMouseClicks(events []ClickEvent) []ClickEvent {
var clicks []ClickEvent
for _, ev := range events {
if ev.Type == TypeClick {
clicks = append(clicks, ev)
}
}
return clicks
}
+10 -14
View File
@@ -1,20 +1,16 @@
module gioui.org module gioui.org
go 1.17 go 1.24.0
require ( require (
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3 eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d gioui.org/shader v1.0.8
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c 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 ( require golang.org/x/net v0.48.0
eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.6
github.com/benoitkugler/textlayout v0.0.10
github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12
github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506
)
require golang.org/x/text v0.3.6 // indirect
+20 -452
View File
@@ -1,453 +1,21 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3 h1:djFprmHZgrSepsHAIRMp5UJn3PzsoTg9drI+BDmif5Q=
eliasnaur.com/font v0.0.0-20220124212145-832bb8fc08c3/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= 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/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y= github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
github.com/benoitkugler/pstokenizer v1.0.0/go.mod h1:l1G2Voirz0q/jj0TQfabNxVsa8HZXh/VMxFSRALWTiE=
github.com/benoitkugler/textlayout v0.0.5/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8=
github.com/benoitkugler/textlayout v0.0.10 h1:uIaQgH4pBFw1LQ0tPkfjgxo94WYcckzzQaB41L2X84w=
github.com/benoitkugler/textlayout v0.0.10/go.mod h1:puH4v13Uz7uIhIH0XMk5jgc8U3MXcn5r3VlV9K8n0D8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21/go.mod h1:po7NpZ/QiTKzBKyrsEAxwnTamCoh8uDk/egRpQ7siIc=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12 h1:1bjaB/5IIicfKpP4k0s30T2WEw//Kh00zULa8DQ0cxA=
github.com/gioui/uax v0.2.1-0.20220325163150-e3d987515a12/go.mod h1:kDhBRTA/i3H46PVdhqcw26TdGSIj42TOKNWKY+Kipnw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506 h1:1TPz/Gn/MsXwJ6bEtI9wdkPcQYr2X3V9I+wz4wPYUdY=
github.com/go-text/typesetting v0.0.0-20220112121102-58fe93c84506/go.mod h1:R0mlTNeyszZ/tKQhbZA7SRGjx+OHsmNzgN2jTV7yZcs=
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k=
github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w=
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis=
github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA=
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3 h1:IlrJD2AM5p8JhN/wVny9jt6gJ9hut2VALhSeZ3SYluk=
golang.org/x/exp v0.0.0-20210722180016-6781d3edade3/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210504121937-7319ad40d33e/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=
+39 -28
View File
@@ -5,12 +5,21 @@ package gpu
import ( import (
"fmt" "fmt"
"gioui.org/f32" "gioui.org/internal/f32"
) )
type resourceCache struct { type textureCacheKey struct {
res map[interface{}]resource filter byte
newRes map[interface{}]resource handle any
}
type textureCache struct {
res map[textureCacheKey]resourceCacheValue
}
type resourceCacheValue struct {
used bool
resource resource
} }
// opCache is like a resourceCache but using concrete types and a // opCache is like a resourceCache but using concrete types and a
@@ -33,48 +42,50 @@ type opCacheValue struct {
keep bool keep bool
} }
func newResourceCache() *resourceCache { func newTextureCache() *textureCache {
return &resourceCache{ return &textureCache{
res: make(map[interface{}]resource), res: make(map[textureCacheKey]resourceCacheValue),
newRes: make(map[interface{}]resource),
} }
} }
func (r *resourceCache) get(key interface{}) (resource, bool) { func (r *textureCache) get(key textureCacheKey) (resource, bool) {
v, exists := r.res[key] v, exists := r.res[key]
if exists { if !exists {
r.newRes[key] = v return nil, false
} }
return v, exists if !v.used {
v.used = true
r.res[key] = v
}
return v.resource, exists
} }
func (r *resourceCache) put(key interface{}, val resource) { func (r *textureCache) put(key textureCacheKey, val resource) {
if _, exists := r.newRes[key]; exists { v, exists := r.res[key]
panic(fmt.Errorf("key exists, %p", key)) if exists && v.used {
panic(fmt.Errorf("key exists, %v", key))
} }
r.res[key] = val v.used = true
r.newRes[key] = val v.resource = val
r.res[key] = v
} }
func (r *resourceCache) frame() { func (r *textureCache) frame() {
for k, v := range r.res { for k, v := range r.res {
if _, exists := r.newRes[k]; !exists { if v.used {
v.used = false
r.res[k] = v
} else {
delete(r.res, k) delete(r.res, k)
v.release() v.resource.release()
} }
} }
for k, v := range r.newRes {
delete(r.newRes, k)
r.res[k] = v
}
} }
func (r *resourceCache) release() { func (r *textureCache) release() {
r.frame()
for _, v := range r.res { for _, v := range r.res {
v.release() v.resource.release()
} }
r.newRes = nil
r.res = nil r.res = nil
} }
+41 -9
View File
@@ -1,7 +1,10 @@
package gpu package gpu
import ( import (
"gioui.org/f32" "encoding/binary"
"math"
"gioui.org/internal/f32"
"gioui.org/internal/stroke" "gioui.org/internal/stroke"
) )
@@ -9,19 +12,48 @@ type quadSplitter struct {
bounds f32.Rectangle bounds f32.Rectangle
contour uint32 contour uint32
d *drawOps d *drawOps
// scratch space used by calls to stroke.SplitCubic
scratch []stroke.QuadSegment
} }
func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) { func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) {
// NW. // inlined code:
encodeVertex(data, meta, -1, 1, from, ctrl, to) // encodeVertex(data, meta, 1, -1, from, ctrl, to)
// NE. // encodeVertex(data[vertStride:], 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)
// SW. // encodeVertex(data[vertStride*3:], meta, -1, 1, from, ctrl, to)
encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to) // this code needs to stay in sync with `vertex.encode`.
// SE.
encodeVertex(data[vertStride*3:], meta, 1, -1, from, ctrl, to) bo := binary.LittleEndian
data = data[:vertStride*4]
// encode the main template
bo.PutUint32(data[4:8], meta)
bo.PutUint32(data[8:12], math.Float32bits(from.X))
bo.PutUint32(data[12:16], math.Float32bits(from.Y))
bo.PutUint32(data[16:20], math.Float32bits(ctrl.X))
bo.PutUint32(data[20:24], math.Float32bits(ctrl.Y))
bo.PutUint32(data[24:28], math.Float32bits(to.X))
bo.PutUint32(data[28:32], math.Float32bits(to.Y))
copy(data[vertStride*1:vertStride*2], data[vertStride*0:vertStride*1])
copy(data[vertStride*2:vertStride*3], data[vertStride*0:vertStride*1])
copy(data[vertStride*3:vertStride*4], data[vertStride*0:vertStride*1])
bo.PutUint32(data[vertStride*0:vertStride*0+4], math.Float32bits(nwCorner))
bo.PutUint32(data[vertStride*1:vertStride*1+4], math.Float32bits(neCorner))
bo.PutUint32(data[vertStride*2:vertStride*2+4], math.Float32bits(swCorner))
bo.PutUint32(data[vertStride*3:vertStride*3+4], math.Float32bits(seCorner))
} }
const (
nwCorner = 1*0.5 + 0*0.25
neCorner = 1*0.5 + 1*0.25
swCorner = 0*0.5 + 0*0.25
seCorner = 0*0.5 + 1*0.25
)
func encodeVertex(data []byte, meta uint32, cornerx, cornery int16, from, ctrl, to f32.Point) { func encodeVertex(data []byte, meta uint32, cornerx, cornery int16, from, ctrl, to f32.Point) {
var corner float32 var corner float32
if cornerx == 1 { if cornerx == 1 {
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: Unlicense OR MIT
package gpu
import (
"testing"
"gioui.org/internal/f32"
)
func BenchmarkEncodeQuadTo(b *testing.B) {
var data [vertStride * 4]byte
for i := 0; b.Loop(); i++ {
v := float32(i)
encodeQuadTo(data[:], 123,
f32.Point{X: v, Y: v},
f32.Point{X: v, Y: v},
f32.Point{X: v, Y: v},
)
}
}
-2219
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)
}
+387 -192
View File
@@ -9,18 +9,19 @@ package gpu
import ( import (
"encoding/binary" "encoding/binary"
"errors"
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"math" "math"
"os"
"reflect" "reflect"
"slices"
"time" "time"
"unsafe" "unsafe"
"gioui.org/f32"
"gioui.org/gpu/internal/driver" "gioui.org/gpu/internal/driver"
"gioui.org/internal/byteslice" "gioui.org/internal/byteslice"
"gioui.org/internal/f32"
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/internal/ops" "gioui.org/internal/ops"
"gioui.org/internal/scene" "gioui.org/internal/scene"
@@ -44,14 +45,10 @@ type GPU interface {
Clear(color color.NRGBA) Clear(color color.NRGBA)
// Frame draws the graphics operations from op into a viewport of target. // Frame draws the graphics operations from op into a viewport of target.
Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error 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 { type gpu struct {
cache *resourceCache cache *textureCache
profile string profile string
timers *timers timers *timers
@@ -68,22 +65,39 @@ type renderer struct {
pather *pather pather *pather
packer packer packer packer
intersections packer intersections packer
layers packer
layerFBOs fboSet
} }
type drawOps struct { type drawOps struct {
profile bool reader ops.Reader
reader ops.Reader states []f32.Affine2D
states []f32.Affine2D transStack []f32.Affine2D
transStack []f32.Affine2D layers []opacityLayer
vertCache []byte opacityStack []int
viewport image.Point vertCache []byte
clear bool viewport image.Point
clearColor f32color.RGBA clear bool
imageOps []imageOp clearColor f32color.RGBA
pathOps []*pathOp imageOps []imageOp
pathOpCache []pathOp pathOps []*pathOp
qs quadSplitter pathOpCache []pathOp
pathCache *opCache qs quadSplitter
pathCache *opCache
}
type opacityLayer struct {
opacity float32
parent int
// depth of the opacity stack. Layers of equal depth are
// independent and may be packed into one atlas.
depth int
// opStart and opEnd denote the range of drawOps.imageOps
// that belong to the layer.
opStart, opEnd int
// clip of the layer operations.
clip image.Rectangle
place placement
} }
type drawState struct { type drawState struct {
@@ -127,7 +141,12 @@ type imageOp struct {
clip image.Rectangle clip image.Rectangle
material material material material
clipType clipType clipType clipType
place placement // place is either a placement in the path fbos or intersection fbos,
// depending on clipType.
place placement
// layerOps is the number of operations this
// operation replaces.
layerOps int
} }
func decodeStrokeOp(data []byte) float32 { func decodeStrokeOp(data []byte) float32 {
@@ -154,17 +173,25 @@ type material struct {
// For materialTypeColor. // For materialTypeColor.
color f32color.RGBA color f32color.RGBA
// For materialTypeLinearGradient. // For materialTypeLinearGradient.
color1 f32color.RGBA color1 f32color.RGBA
color2 f32color.RGBA color2 f32color.RGBA
opacity float32
// For materialTypeTexture. // For materialTypeTexture.
data imageOpData data imageOpData
tex driver.Texture
uvTrans f32.Affine2D uvTrans f32.Affine2D
} }
const (
filterLinear = 0
filterNearest = 1
)
// imageOpData is the shadow of paint.ImageOp. // imageOpData is the shadow of paint.ImageOp.
type imageOpData struct { type imageOpData struct {
src *image.RGBA src *image.RGBA
handle interface{} handle any
filter byte
} }
type linearGradientOpData struct { type linearGradientOpData struct {
@@ -174,7 +201,7 @@ type linearGradientOpData struct {
color2 color.NRGBA color2 color.NRGBA
} }
func decodeImageOp(data []byte, refs []interface{}) imageOpData { func decodeImageOp(data []byte, refs []any) imageOpData {
handle := refs[1] handle := refs[1]
if handle == nil { if handle == nil {
return imageOpData{} return imageOpData{}
@@ -182,10 +209,12 @@ func decodeImageOp(data []byte, refs []interface{}) imageOpData {
return imageOpData{ return imageOpData{
src: refs[0].(*image.RGBA), src: refs[0].(*image.RGBA),
handle: handle, handle: handle,
filter: data[1],
} }
} }
func decodeColorOp(data []byte) color.NRGBA { func decodeColorOp(data []byte) color.NRGBA {
data = data[:ops.TypeColorLen]
return color.NRGBA{ return color.NRGBA{
R: data[1], R: data[1],
G: data[2], G: data[2],
@@ -195,6 +224,7 @@ func decodeColorOp(data []byte) color.NRGBA {
} }
func decodeLinearGradientOp(data []byte) linearGradientOpData { func decodeLinearGradientOp(data []byte) linearGradientOpData {
data = data[:ops.TypeLinearGradientLen]
bo := binary.LittleEndian bo := binary.LittleEndian
return linearGradientOpData{ return linearGradientOpData{
stop1: f32.Point{ stop1: f32.Point{
@@ -220,8 +250,6 @@ func decodeLinearGradientOp(data []byte) linearGradientOpData {
} }
} }
type clipType uint8
type resource interface { type resource interface {
release() release()
} }
@@ -234,7 +262,7 @@ type texture struct {
type blitter struct { type blitter struct {
ctx driver.Device ctx driver.Device
viewport image.Point viewport image.Point
pipelines [3]*pipeline pipelines [2][3]*pipeline
colUniforms *blitColUniforms colUniforms *blitColUniforms
texUniforms *blitTexUniforms texUniforms *blitTexUniforms
linearGradientUniforms *blitLinearGradientUniforms linearGradientUniforms *blitLinearGradientUniforms
@@ -271,6 +299,9 @@ type blitUniforms struct {
transform [4]float32 transform [4]float32
uvTransformR1 [4]float32 uvTransformR1 [4]float32
uvTransformR2 [4]float32 uvTransformR2 [4]float32
opacity float32
fbo float32
_ [2]float32
} }
type colorUniforms struct { type colorUniforms struct {
@@ -282,7 +313,7 @@ type gradientUniforms struct {
color2 f32color.RGBA color2 f32color.RGBA
} }
type materialType uint8 type clipType uint8
const ( const (
clipTypeNone clipType = iota clipTypeNone clipType = iota
@@ -290,6 +321,8 @@ const (
clipTypeIntersection clipTypeIntersection
) )
type materialType uint8
const ( const (
materialColor materialType = iota materialColor materialType = iota
materialLinearGradient materialLinearGradient
@@ -311,18 +344,17 @@ func New(api API) (GPU, error) {
func NewWithDevice(d driver.Device) (GPU, error) { func NewWithDevice(d driver.Device) (GPU, error) {
d.BeginFrame(nil, false, image.Point{}) d.BeginFrame(nil, false, image.Point{})
defer d.EndFrame() defer d.EndFrame()
forceCompute := os.Getenv("GIORENDERER") == "forcecompute"
feats := d.Caps().Features feats := d.Caps().Features
switch { switch {
case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB): case feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB):
return newGPU(d) return newGPU(d)
} }
return newCompute(d) return nil, errors.New("no available GPU driver")
} }
func newGPU(ctx driver.Device) (*gpu, error) { func newGPU(ctx driver.Device) (*gpu, error) {
g := &gpu{ g := &gpu{
cache: newResourceCache(), cache: newTextureCache(),
} }
g.drawOps.pathCache = newOpCache() g.drawOps.pathCache = newOpCache()
if err := g.init(ctx); err != nil { if err := g.init(ctx); err != nil {
@@ -362,7 +394,7 @@ func (g *gpu) collect(viewport image.Point, frameOps *op.Ops) {
g.renderer.pather.viewport = viewport g.renderer.pather.viewport = viewport
g.drawOps.reset(viewport) g.drawOps.reset(viewport)
g.drawOps.collect(frameOps, 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.frameStart = time.Now()
g.timers = newTimers(g.ctx) g.timers = newTimers(g.ctx)
g.stencilTimer = g.timers.newTimer() g.stencilTimer = g.timers.newTimer()
@@ -388,7 +420,9 @@ func (g *gpu) frame(target RenderTarget) error {
g.stencilTimer.end() g.stencilTimer.end()
g.coverTimer.begin() g.coverTimer.begin()
g.renderer.uploadImages(g.cache, g.drawOps.imageOps) 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.drawOps.layers, g.drawOps.imageOps)
d := driver.LoadDesc{ d := driver.LoadDesc{
ClearColor: g.drawOps.clearColor, ClearColor: g.drawOps.clearColor,
} }
@@ -398,14 +432,14 @@ func (g *gpu) frame(target RenderTarget) error {
} }
g.ctx.BeginRenderPass(defFBO, d) g.ctx.BeginRenderPass(defFBO, d)
g.ctx.Viewport(0, 0, viewport.X, viewport.Y) g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
g.renderer.drawOps(g.cache, g.drawOps.imageOps) g.renderer.drawOps(false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
g.coverTimer.end() g.coverTimer.end()
g.ctx.EndRenderPass() g.ctx.EndRenderPass()
g.cleanupTimer.begin() g.cleanupTimer.begin()
g.cache.frame() g.cache.frame()
g.drawOps.pathCache.frame() g.drawOps.pathCache.frame()
g.cleanupTimer.end() 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 st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
ft := st + covt + cleant ft := st + covt + cleant
q := 100 * time.Microsecond q := 100 * time.Microsecond
@@ -421,20 +455,38 @@ func (g *gpu) Profile() string {
return g.profile 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 var tex *texture
t, exists := cache.get(data.handle) t, exists := cache.get(key)
if !exists { if !exists {
t = &texture{ t = &texture{
src: data.src, src: data.src,
} }
cache.put(data.handle, t) cache.put(key, t)
} }
tex = t.(*texture) tex = t.(*texture)
if tex.tex != nil { if tex.tex != nil {
return tex.tex return tex.tex
} }
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA, data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinear, 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 { if err != nil {
panic(err) panic(err)
} }
@@ -462,15 +514,18 @@ func newRenderer(ctx driver.Device) *renderer {
if cap := 8192; maxDim > cap { if cap := 8192; maxDim > cap {
maxDim = cap maxDim = cap
} }
d := image.Pt(maxDim, maxDim)
r.packer.maxDims = image.Pt(maxDim, maxDim) r.packer.maxDims = d
r.intersections.maxDims = image.Pt(maxDim, maxDim) r.intersections.maxDims = d
r.layers.maxDims = d
return r return r
} }
func (r *renderer) release() { func (r *renderer) release() {
r.pather.release() r.pather.release()
r.blitter.release() r.blitter.release()
r.layerFBOs.delete(r.ctx, 0)
} }
func newBlitter(ctx driver.Device) *blitter { func newBlitter(ctx driver.Device) *blitter {
@@ -493,7 +548,7 @@ func newBlitter(ctx driver.Device) *blitter {
b.texUniforms = new(blitTexUniforms) b.texUniforms = new(blitTexUniforms)
b.linearGradientUniforms = new(blitLinearGradientUniforms) b.linearGradientUniforms = new(blitLinearGradientUniforms)
pipelines, err := createColorPrograms(ctx, gio.Shader_blit_vert, gio.Shader_blit_frag, 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 { if err != nil {
panic(err) panic(err)
@@ -505,12 +560,24 @@ func newBlitter(ctx driver.Device) *blitter {
func (b *blitter) release() { func (b *blitter) release() {
b.quadVerts.Release() b.quadVerts.Release()
for _, p := range b.pipelines { 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) { func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]any) (pipelines [2][3]*pipeline, err error) {
var pipelines [3]*pipeline defer func() {
if err != nil {
for _, p := range pipelines {
for _, p := range p {
if p != nil {
p.Release()
}
}
}
}
}()
blend := driver.BlendDesc{ blend := driver.BlendDesc{
Enable: true, Enable: true,
SrcFactor: driver.BlendFactorOne, SrcFactor: driver.BlendFactorOne,
@@ -528,86 +595,76 @@ func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.
return pipelines, err return pipelines, err
} }
defer vsh.Release() defer vsh.Release()
{ for i, format := range []driver.TextureFormat{driver.TextureFormatOutput, driver.TextureFormatSRGBA} {
fsh, err := b.NewFragmentShader(fsSrc[materialTexture]) {
if err != nil { fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
return pipelines, err 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{ var vertBuffer *uniformBuffer
VertexShader: vsh, fsh, err := b.NewFragmentShader(fsSrc[materialColor])
FragmentShader: fsh, if err != nil {
BlendDesc: blend, return pipelines, err
VertexLayout: layout, }
PixelFormat: driver.TextureFormatOutput, defer fsh.Release()
Topology: driver.TopologyTriangleStrip, pipe, err := b.NewPipeline(driver.PipelineDesc{
}) VertexShader: vsh,
if err != nil { FragmentShader: fsh,
return pipelines, err 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 { var vertBuffer *uniformBuffer
vertBuffer = newUniformBuffer(b, u) 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 return pipelines, nil
} }
@@ -695,8 +752,8 @@ func (r *renderer) intersectPath(p *pathOp, clip image.Rectangle) {
} }
fbo := r.pather.stenciler.cover(p.place.Idx) fbo := r.pather.stenciler.cover(p.place.Idx)
r.ctx.BindTexture(0, fbo.tex) r.ctx.BindTexture(0, fbo.tex)
coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.size) coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size)
subScale, subOff := texSpaceTransform(layout.FRect(sub), p.clip.Size()) subScale, subOff := texSpaceTransform(f32.FRect(sub), p.clip.Size())
r.pather.stenciler.ipipeline.uniforms.vert.uvTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y} r.pather.stenciler.ipipeline.uniforms.vert.uvTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
r.pather.stenciler.ipipeline.uniforms.vert.subUVTransform = [4]float32{subScale.X, subScale.Y, subOff.X, subOff.Y} r.pather.stenciler.ipipeline.uniforms.vert.subUVTransform = [4]float32{subScale.X, subScale.Y, subOff.X, subOff.Y}
r.pather.stenciler.ipipeline.pipeline.UploadUniforms(r.ctx) r.pather.stenciler.ipipeline.pipeline.UploadUniforms(r.ctx)
@@ -745,8 +802,7 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
ops = ops[:len(ops)-1] ops = ops[:len(ops)-1]
continue continue
} }
sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()} place, ok := r.packer.add(p.clip.Size())
place, ok := r.packer.add(sz)
if !ok { if !ok {
// The clip area is at most the entire screen. Hopefully no // The clip area is at most the entire screen. Hopefully no
// screen is larger than GL_MAX_TEXTURE_SIZE. // screen is larger than GL_MAX_TEXTURE_SIZE.
@@ -758,14 +814,92 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
*pops = ops *pops = ops
} }
func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
// Make every layer bounds contain nested layers; cull empty layers.
for i := len(layers) - 1; i >= 0; i-- {
l := layers[i]
if l.parent != -1 {
b := layers[l.parent].clip
layers[l.parent].clip = b.Union(l.clip)
}
if l.clip.Empty() {
layers = slices.Delete(layers, i, i+1)
}
}
// Pack layers.
r.layers.clear()
depth := 0
for i := range layers {
l := &layers[i]
// Only layers of the same depth may be packed together.
if l.depth != depth {
r.layers.newPage()
}
place, ok := r.layers.add(l.clip.Size())
if !ok {
// The layer area is at most the entire screen. Hopefully no
// screen is larger than GL_MAX_TEXTURE_SIZE.
panic(fmt.Errorf("layer size %v is larger than maximum texture size %v", l.clip.Size(), r.layers.maxDims))
}
l.place = place
}
return layers
}
func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
if len(r.layers.sizes) == 0 {
return
}
fbo := -1
r.layerFBOs.resize(r.ctx, driver.TextureFormatSRGBA, r.layers.sizes)
for i := len(layers) - 1; i >= 0; i-- {
l := layers[i]
if fbo != l.place.Idx {
if fbo != -1 {
r.ctx.EndRenderPass()
r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
}
fbo = l.place.Idx
f := r.layerFBOs.fbos[fbo]
r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear})
}
v := image.Rectangle{
Min: l.place.Pos,
Max: l.place.Pos.Add(l.clip.Size()),
}
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy())
f := r.layerFBOs.fbos[fbo]
r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
sr := f32.FRect(v)
uvScale, uvOffset := texSpaceTransform(sr, f.size)
uvTrans := f32.AffineId().Scale(f32.Point{}, uvScale).Offset(uvOffset)
// Replace layer ops with one textured op.
ops[l.opStart] = imageOp{
clip: l.clip,
material: material{
material: materialTexture,
tex: f.tex,
uvTrans: uvTrans,
opacity: l.opacity,
},
layerOps: l.opEnd - l.opStart - 1,
}
}
if fbo != -1 {
r.ctx.EndRenderPass()
r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
}
}
func (d *drawOps) reset(viewport image.Point) { func (d *drawOps) reset(viewport image.Point) {
d.profile = false
d.viewport = viewport d.viewport = viewport
d.imageOps = d.imageOps[:0] d.imageOps = d.imageOps[:0]
d.pathOps = d.pathOps[:0] d.pathOps = d.pathOps[:0]
d.pathOpCache = d.pathOpCache[:0] d.pathOpCache = d.pathOpCache[:0]
d.vertCache = d.vertCache[:0] d.vertCache = d.vertCache[:0]
d.transStack = d.transStack[:0] d.transStack = d.transStack[:0]
d.layers = d.layers[:0]
d.opacityStack = d.opacityStack[:0]
} }
func (d *drawOps) collect(root *op.Ops, viewport image.Point) { func (d *drawOps) collect(root *op.Ops, viewport image.Point) {
@@ -798,7 +932,7 @@ func (d *drawOps) newPathOp() *pathOp {
return &d.pathOpCache[len(d.pathOpCache)-1] 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 := d.newPathOp()
*npath = pathOp{ *npath = pathOp{
parent: state.cpath, parent: state.cpath,
@@ -821,18 +955,11 @@ func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds
state.cpath = npath state.cpath = npath
} }
// split a transform into two parts, one which is pure offset and the
// other representing the scaling, shearing and rotation part
func splitTransform(t f32.Affine2D) (srs f32.Affine2D, offset f32.Point) {
sx, hx, ox, hy, sy, oy := t.Elems()
offset = f32.Point{X: ox, Y: oy}
srs = f32.NewAffine2D(sx, hx, 0, hy, sy, 0)
return
}
func (d *drawOps) save(id int, state f32.Affine2D) { func (d *drawOps) save(id int, state f32.Affine2D) {
if extra := id - len(d.states) + 1; extra > 0 { 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 d.states[id] = state
} }
@@ -847,12 +974,13 @@ func (k opKey) SetTransform(t f32.Affine2D) opKey {
} }
func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) { func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
var ( var quads quadsOp
quads quadsOp state := drawState{
state drawState t: f32.AffineId(),
) }
reset := func() { reset := func() {
state = drawState{ state = drawState{
t: f32.AffineId(),
color: color.NRGBA{A: 0xff}, color: color.NRGBA{A: 0xff},
} }
} }
@@ -860,8 +988,6 @@ func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
loop: loop:
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() { for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch ops.OpType(encOp.Data[0]) { switch ops.OpType(encOp.Data[0]) {
case ops.TypeProfile:
d.profile = true
case ops.TypeTransform: case ops.TypeTransform:
dop, push := ops.DecodeTransform(encOp.Data) dop, push := ops.DecodeTransform(encOp.Data)
if push { if push {
@@ -873,6 +999,27 @@ loop:
state.t = d.transStack[n-1] state.t = d.transStack[n-1]
d.transStack = d.transStack[:n-1] d.transStack = d.transStack[:n-1]
case ops.TypePushOpacity:
opacity := ops.DecodeOpacity(encOp.Data)
parent := -1
depth := len(d.opacityStack)
if depth > 0 {
parent = d.opacityStack[depth-1]
}
lidx := len(d.layers)
d.layers = append(d.layers, opacityLayer{
opacity: opacity,
parent: parent,
depth: depth,
opStart: len(d.imageOps),
})
d.opacityStack = append(d.opacityStack, lidx)
case ops.TypePopOpacity:
n := len(d.opacityStack)
idx := d.opacityStack[n-1]
d.layers[idx].opEnd = len(d.imageOps)
d.opacityStack = d.opacityStack[:n-1]
case ops.TypeStroke: case ops.TypeStroke:
quads.key.strokeWidth = decodeStrokeOp(encOp.Data) quads.key.strokeWidth = decodeStrokeOp(encOp.Data)
@@ -888,8 +1035,8 @@ loop:
var op ops.ClipOp var op ops.ClipOp
op.Decode(encOp.Data) op.Decode(encOp.Data)
quads.key.outline = op.Outline quads.key.outline = op.Outline
bounds := layout.FRect(op.Bounds) bounds := f32.FRect(op.Bounds)
trans, off := splitTransform(state.t) trans, off := transformOffset(state.t)
if len(quads.aux) > 0 { if len(quads.aux) > 0 {
// There is a clipping path, build the gpu data and update the // 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 // cache key such that it will be equal only if the transform is the
@@ -912,8 +1059,9 @@ loop:
} else { } else {
quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans) quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans)
quads.key = opKey{Key: encOp.Key} 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{} quads = quadsOp{}
case ops.TypePopClip: case ops.TypePopClip:
state.cpath = state.cpath.parent state.cpath = state.cpath.parent
@@ -935,7 +1083,7 @@ loop:
// Transform (if needed) the painting rectangle and if so generate a clip path, // 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 // for those cases also compute a partialTrans that maps texture coordinates between
// the new bounding rectangle and the transformed original paint rectangle. // the new bounding rectangle and the transformed original paint rectangle.
t, off := splitTransform(state.t) t, off := transformOffset(state.t)
// Fill the clip area, unless the material is a (bounded) image. // Fill the clip area, unless the material is a (bounded) image.
// TODO: Find a tighter bound. // TODO: Find a tighter bound.
inf := float32(1e6) inf := float32(1e6)
@@ -957,15 +1105,15 @@ loop:
// The paint operation is sheared or rotated, add a clip path representing // The paint operation is sheared or rotated, add a clip path representing
// this transformed rectangle. // this transformed rectangle.
k := opKey{Key: encOp.Key} k := opKey{Key: encOp.Key}
k.SetTransform(t) // TODO: This call has no effect. k = k.SetTransform(t)
d.addClipPath(&state, clipData, k, bnd, off, false) d.addClipPath(&state, clipData, k, bnd, off)
} }
bounds := cl.Round() bounds := cl.Round()
mat := state.materialFor(bnd, off, partialTrans, bounds) mat := state.materialFor(bnd, off, partialTrans, bounds)
rect := state.cpath == nil || state.cpath.rect rect := state.cpath == nil || state.cpath.rect
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) { if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) && len(d.opacityStack) == 0 {
// The image is a uniform opaque color and takes up the whole screen. // The image is a uniform opaque color and takes up the whole screen.
// Scrap images up to and including this image and set clear color. // Scrap images up to and including this image and set clear color.
d.imageOps = d.imageOps[:0] d.imageOps = d.imageOps[:0]
@@ -978,6 +1126,15 @@ loop:
clip: bounds, clip: bounds,
material: mat, material: mat,
} }
if n := len(d.opacityStack); n > 0 {
idx := d.opacityStack[n-1]
lb := d.layers[idx].clip
if lb.Empty() {
d.layers[idx].clip = img.clip
} else {
d.layers[idx].clip = lb.Union(img.clip)
}
}
d.imageOps = append(d.imageOps, img) d.imageOps = append(d.imageOps, img)
if clipData != nil { if clipData != nil {
@@ -1007,7 +1164,10 @@ func expandPathOp(p *pathOp, clip image.Rectangle) {
} }
func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle) material { func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle) material {
var m material m := material{
opacity: 1.,
uvTrans: f32.AffineId(),
}
switch d.matType { switch d.matType {
case materialColor: case materialColor:
m.material = materialColor m.material = materialColor
@@ -1040,30 +1200,31 @@ 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.Min.Y += float32(clip.Min.Y-dr.Min.Y) * sdy / dy
sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy sr.Max.Y -= float32(dr.Max.Y-clip.Max.Y) * sdy / dy
uvScale, uvOffset := texSpaceTransform(sr, sz) 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 m.data = d.image
} }
return m return m
} }
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) { func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) {
for _, img := range ops { for i := range ops {
img := &ops[i]
m := img.material m := img.material
if m.material == materialTexture { if m.material == materialTexture {
r.texHandle(cache, m.data) img.material.tex = r.texHandle(cache, m.data)
} }
} }
} }
func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) { func (r *renderer) prepareDrawOps(ops []imageOp) {
for _, img := range ops { for _, img := range ops {
m := img.material m := img.material
switch m.material { switch m.material {
case materialTexture: case materialTexture:
r.ctx.PrepareTexture(r.texHandle(cache, m.data)) r.ctx.PrepareTexture(m.tex)
} }
var fbo stencilFBO var fbo FBO
switch img.clipType { switch img.clipType {
case clipTypeNone: case clipTypeNone:
continue continue
@@ -1076,24 +1237,30 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
} }
} }
func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) { func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp) {
var coverTex driver.Texture var coverTex driver.Texture
for _, img := range ops { for i := 0; i < len(ops); i++ {
img := ops[i]
i += img.layerOps
m := img.material m := img.material
switch m.material { switch m.material {
case materialTexture: case materialTexture:
r.ctx.BindTexture(0, r.texHandle(cache, m.data)) r.ctx.BindTexture(0, m.tex)
} }
drc := img.clip drc := img.clip.Add(opOff)
scale, off := clipSpaceTransform(drc, r.blitter.viewport) scale, off := clipSpaceTransform(drc, viewport)
var fbo stencilFBO var fbo FBO
fboIdx := 0
if isFBO {
fboIdx = 1
}
switch img.clipType { switch img.clipType {
case clipTypeNone: case clipTypeNone:
p := r.blitter.pipelines[m.material] p := r.blitter.pipelines[fboIdx][m.material]
r.ctx.BindPipeline(p.pipeline) r.ctx.BindPipeline(p.pipeline)
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0) r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
r.blitter.blit(m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans) r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans)
continue continue
case clipTypePath: case clipTypePath:
fbo = r.pather.stenciler.cover(img.place.Idx) fbo = r.pather.stenciler.cover(img.place.Idx)
@@ -1108,16 +1275,20 @@ func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
Min: img.place.Pos, Min: img.place.Pos,
Max: img.place.Pos.Add(drc.Size()), Max: img.place.Pos.Add(drc.Size()),
} }
coverScale, coverOff := texSpaceTransform(layout.FRect(uv), fbo.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.BindPipeline(p.pipeline)
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0) r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
r.pather.cover(m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff) r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
} }
} }
func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) { 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) b.ctx.BindPipeline(p.pipeline)
var uniforms *blitUniforms var uniforms *blitUniforms
switch mat { switch mat {
@@ -1126,18 +1297,23 @@ func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.
uniforms = &b.colUniforms.blitUniforms uniforms = &b.colUniforms.blitUniforms
case materialTexture: case materialTexture:
t1, t2, t3, t4, t5, t6 := uvTrans.Elems() t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
b.texUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
b.texUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
uniforms = &b.texUniforms.blitUniforms uniforms = &b.texUniforms.blitUniforms
uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
case materialLinearGradient: case materialLinearGradient:
b.linearGradientUniforms.color1 = col1 b.linearGradientUniforms.color1 = col1
b.linearGradientUniforms.color2 = col2 b.linearGradientUniforms.color2 = col2
t1, t2, t3, t4, t5, t6 := uvTrans.Elems() t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
b.linearGradientUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
b.linearGradientUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
uniforms = &b.linearGradientUniforms.blitUniforms uniforms = &b.linearGradientUniforms.blitUniforms
uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
} }
uniforms.fbo = 0
if fbo {
uniforms.fbo = 1
}
uniforms.opacity = opacity
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
p.UploadUniforms(b.ctx) p.UploadUniforms(b.ctx)
b.ctx.DrawArrays(0, 4) b.ctx.DrawArrays(0, 4)
@@ -1145,7 +1321,7 @@ func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.
// newUniformBuffer creates a new GPU uniform buffer backed by the // newUniformBuffer creates a new GPU uniform buffer backed by the
// structure uniformBlock points to. // structure uniformBlock points to.
func newUniformBuffer(b driver.Device, uniformBlock interface{}) *uniformBuffer { func newUniformBuffer(b driver.Device, uniformBlock any) *uniformBuffer {
ref := reflect.ValueOf(uniformBlock) ref := reflect.ValueOf(uniformBlock)
// Determine the size of the uniforms structure, *uniforms. // Determine the size of the uniforms structure, *uniforms.
size := ref.Elem().Type().Size() size := ref.Elem().Type().Size()
@@ -1199,7 +1375,7 @@ func gradientSpaceTransform(clip image.Rectangle, off f32.Point, stop1, stop2 f3
// TODO: optimize // TODO: optimize
zp := f32.Point{} zp := f32.Point{}
return f32.Affine2D{}. return f32.AffineId().
Scale(zp, layout.FPt(clip.Size())). // scale to pixel space 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(off).Add(layout.FPt(clip.Min))). // offset to clip space
Offset(zp.Sub(stop1)). // offset to first stop point Offset(zp.Sub(stop1)). // offset to first stop point
@@ -1313,7 +1489,7 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str
// as needed and feeds them to the supplied splitter. // as needed and feeds them to the supplied splitter.
func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) { func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
for len(pathData) >= scene.CommandSize+4 { for len(pathData) >= scene.CommandSize+4 {
qs.contour = bo.Uint32(pathData) qs.contour = binary.LittleEndian.Uint32(pathData)
cmd := ops.DecodeCommand(pathData[4:]) cmd := ops.DecodeCommand(pathData[4:])
switch cmd.Op() { switch cmd.Op() {
case scene.OpLine: case scene.OpLine:
@@ -1334,7 +1510,9 @@ func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
q = q.Transform(tr) q = q.Transform(tr)
qs.splitAndEncode(q) qs.splitAndEncode(q)
case scene.OpCubic: case scene.OpCubic:
for _, q := range stroke.SplitCubic(scene.DecodeCubic(cmd)) { from, ctrl0, ctrl1, to := scene.DecodeCubic(cmd)
qs.scratch = stroke.SplitCubic(from, ctrl0, ctrl1, to, qs.scratch[:0])
for _, q := range qs.scratch {
q = q.Transform(tr) q = q.Transform(tr)
qs.splitAndEncode(q) qs.splitAndEncode(q)
} }
@@ -1347,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. // 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) { func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, tr f32.Affine2D) (aux []byte, bnd f32.Rectangle, ptr f32.Affine2D) {
if isPureOffset(tr) { ptr = f32.AffineId()
// fast-path to allow blitting of pure rectangles if tr == f32.AffineId() {
_, _, ox, _, _, oy := tr.Elems() // fast-path to allow blitting of pure rectangles.
off := f32.Pt(ox, oy) bnd = r
bnd.Min = r.Min.Add(off)
bnd.Max = r.Max.Add(off)
return return
} }
@@ -1399,10 +1575,29 @@ func (d *drawOps) boundsForTransformedRect(r f32.Rectangle, tr f32.Affine2D) (au
sx, sy := P2.X-P3.X, P2.Y-P3.Y sx, sy := P2.X-P3.X, P2.Y-P3.Y
ptr = f32.NewAffine2D(sx, P2.X-P1.X, P1.X-sx, sy, P2.Y-P1.Y, P1.Y-sy).Invert() 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 { // transformOffset a transform into two parts, one which is pure integer offset
a, b, _, d, e, _ := t.Elems() // and the other representing the scaling, shearing and rotation and fractional
return a == 1 && b == 0 && d == 0 && e == 1 // 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
} }

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