195 Commits

Author SHA1 Message Date
Elias Naur 8e47316332 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-15 13:01:41 +08:00
Chris Waldon 55ae5c5b84 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.

Updates: https://todo.sr.ht/~eliasnaur/gio/599
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2024-07-08 10:12:04 -04: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
135 changed files with 8371 additions and 7122 deletions
+17 -7
View File
@@ -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.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - curl -s https://dl.google.com/go/go1.22.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
+1 -1
View File
@@ -16,7 +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.19.11.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf - curl https://dl.google.com/go/go1.22.2.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- test_gio: | - test_gio: |
cd gio cd gio
go test ./... go test ./...
+3 -6
View File
@@ -40,7 +40,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.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - curl -s https://dl.google.com/go/go1.22.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 .)"
@@ -67,11 +67,6 @@ tasks:
CGO_ENABLED=1 GOARCH=386 go test ./... 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
- 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
@@ -85,6 +80,8 @@ 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: |
+1 -1
View File
@@ -10,7 +10,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.19.11.src.tar.gz | tar -C /home/build/sdk -xzf - curl https://dl.google.com/go/go1.22.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: |
+1 -1
View File
@@ -65,8 +65,8 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
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;
+93 -9
View File
@@ -3,9 +3,16 @@
package app package app
import ( import (
"image"
"os" "os"
"path/filepath" "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
@@ -20,23 +27,88 @@ var extraArgs string
// On Android ID is the package property of AndroidManifest.xml, // On Android ID is the package property of AndroidManifest.xml,
// on iOS ID is the CFBundleIdentifier of the app Info.plist, // on iOS ID is the CFBundleIdentifier of the app Info.plist,
// on Wayland it is the toplevel app_id, // on Wayland it is the toplevel app_id,
// on X11 it is the X11 XClassHint // on X11 it is the X11 XClassHint.
// //
// ID is set by the gogio tool or manually with the -X linker flag. For example, // ID is set by the [gioui.org/cmd/gogio] tool or manually with the -X linker flag. For example,
// //
// go build -ldflags="-X 'gioui.org/app.ID=org.gioui.example.Kitchen'" . // 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 // Note that ID is treated as a constant, and that changing it at runtime
// is not supported. Default value of ID is filepath.Base(os.Args[0]). // is not supported. The default value of ID is filepath.Base(os.Args[0]).
var ID = "" var ID = ""
func init() { // A FrameEvent requests a new frame in the form of a list of
if extraArgs != "" { // operations that describes the window content.
args := strings.Split(extraArgs, "|") type FrameEvent struct {
os.Args = append(os.Args, args...) // Now is the current animation. Use Now instead of time.Now to
// synchronize animation and to avoid the time.Now call overhead.
Now time.Time
// Metric converts device independent dp and sp to device pixels.
Metric unit.Metric
// Size is the dimensions of the window.
Size image.Point
// Insets represent the space occupied by system decorations and controls.
Insets Insets
// Frame completes the FrameEvent by drawing the graphical operations
// from ops into the window.
Frame func(frame *op.Ops)
// Source is the interface between the window and widgets.
Source input.Source
}
// ViewEvent provides handles to the underlying window objects for the
// current display protocol.
type ViewEvent interface {
implementsViewEvent()
ImplementsEvent()
// Valid will return true when the ViewEvent does contains valid handles.
// If a window receives an invalid ViewEvent, it should deinitialize any
// state referring to handles from a previous ViewEvent.
Valid() bool
}
// Insets is the space taken up by
// system decoration such as translucent
// system bars and software keyboards.
type Insets struct {
// Values are in pixels.
Top, Bottom, Left, Right unit.Dp
}
// NewContext is shorthand for
//
// layout.Context{
// Ops: ops,
// Now: e.Now,
// Source: e.Source,
// Metric: e.Metric,
// Constraints: layout.Exact(e.Size),
// }
//
// NewContext calls ops.Reset and adjusts ops for e.Insets.
func NewContext(ops *op.Ops, e FrameEvent) layout.Context {
ops.Reset()
size := e.Size
if e.Insets != (Insets{}) {
left := e.Metric.Dp(e.Insets.Left)
top := e.Metric.Dp(e.Insets.Top)
op.Offset(image.Point{
X: left,
Y: top,
}).Add(ops)
size.X -= left + e.Metric.Dp(e.Insets.Right)
size.Y -= top + e.Metric.Dp(e.Insets.Bottom)
} }
if ID == "" {
ID = filepath.Base(os.Args[0]) return layout.Context{
Ops: ops,
Now: e.Now,
Source: e.Source,
Metric: e.Metric,
Constraints: layout.Exact(size),
} }
} }
@@ -63,3 +135,15 @@ func DataDir() (string, error) {
func Main() { func Main() {
osMain() osMain()
} }
func (FrameEvent) ImplementsEvent() {}
func init() {
if extraArgs != "" {
args := strings.Split(extraArgs, "|")
os.Args = append(os.Args, args...)
}
if ID == "" {
ID = filepath.Base(os.Args[0])
}
}
+5 -5
View File
@@ -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
+13 -19
View File
@@ -8,21 +8,20 @@ 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,7 +31,7 @@ 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
@@ -50,18 +49,13 @@ For example, to display a blank but otherwise functional window:
func main() { func main() {
go func() { go func() {
w := app.NewWindow() w := app.NewWindow()
for range w.Events() { for {
w.Event()
} }
}() }()
app.Main() app.Main()
} }
# Event queue
A FrameEvent's Queue method returns an event.Queue implementation that distributes
incoming events to the event handlers declared in the last frame.
See the gioui.org/io/event package for more information about event handlers.
# Permissions # Permissions
The packages under gioui.org/app/permission should be imported The packages under gioui.org/app/permission should be imported
+4 -6
View File
@@ -17,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
} }
@@ -45,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
} }
@@ -55,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
+11 -1
View File
@@ -69,7 +69,17 @@ func (c *wlContext) Refresh() error {
} }
c.eglWin = eglWin c.eglWin = eglWin
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin))) eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
return c.Context.CreateSurface(eglSurf, width, height) if err := c.Context.CreateSurface(eglSurf); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
return err
}
defer c.Context.ReleaseCurrent()
// We're in charge of the frame callbacks, don't let eglSwapBuffers
// wait for callbacks that may never arrive.
c.Context.EnableVSync(false)
return nil
} }
func (c *wlContext) Lock() error { func (c *wlContext) Lock() error {
+12 -17
View File
@@ -5,8 +5,6 @@
package app package app
import ( import (
"golang.org/x/sys/windows"
"gioui.org/internal/egl" "gioui.org/internal/egl"
) )
@@ -24,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
}, },
}) })
@@ -37,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
} }
+12 -11
View File
@@ -25,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
} }
} }
@@ -37,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 -1
View File
@@ -7,7 +7,7 @@
#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];
} }
+118
View File
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Unlicense OR MIT
package app
import (
"unicode"
"unicode/utf16"
"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
}
+4 -4
View File
@@ -11,8 +11,8 @@ import (
"gioui.org/font" "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"
@@ -31,10 +31,10 @@ func FuzzIME(f *testing.F) {
f.Fuzz(func(t *testing.T, cmds []byte) { f.Fuzz(func(t *testing.T, cmds []byte) {
cache := text.NewShaper(text.WithCollection(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, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
-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"
+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
@@ -244,7 +266,9 @@ const (
WM_MOUSEWHEEL = 0x020A WM_MOUSEWHEEL = 0x020A
WM_MOUSEHWHEEL = 0x020E WM_MOUSEHWHEEL = 0x020E
WM_NCACTIVATE = 0x0086 WM_NCACTIVATE = 0x0086
WM_NCLBUTTONDBLCLK = 0x00A3
WM_NCHITTEST = 0x0084 WM_NCHITTEST = 0x0084
WM_NCCALCSIZE = 0x0083
WM_PAINT = 0x000F WM_PAINT = 0x000F
WM_QUIT = 0x0012 WM_QUIT = 0x0012
WM_SETCURSOR = 0x0020 WM_SETCURSOR = 0x0020
@@ -323,6 +347,7 @@ var (
_DispatchMessage = user32.NewProc("DispatchMessageW") _DispatchMessage = user32.NewProc("DispatchMessageW")
_EmptyClipboard = user32.NewProc("EmptyClipboard") _EmptyClipboard = user32.NewProc("EmptyClipboard")
_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")
@@ -379,6 +404,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) {
@@ -430,6 +458,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 {
@@ -444,6 +480,12 @@ func GetWindowRect(hwnd syscall.Handle) Rect {
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 {
+67 -8
View File
@@ -238,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
@@ -255,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:
@@ -297,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
@@ -309,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.
@@ -3,7 +3,7 @@
//go:build darwin && ios //go:build darwin && ios
// +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,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
package log package app
import ( import (
"log" "log"
+2 -1
View File
@@ -60,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];
} }
} }
+4 -1
View File
@@ -21,7 +21,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);
} }
} }
+6 -2
View File
@@ -12,9 +12,13 @@ package app
#import <QuartzCore/CAMetalLayer.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;
} }
} }
+165 -21
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"
@@ -43,6 +45,8 @@ type Config struct {
CustomRenderer bool CustomRenderer bool
// Decorated reports whether window decorations are provided automatically. // Decorated reports whether window decorations are provided automatically.
Decorated bool Decorated bool
// Focused reports whether has the keyboard focus.
Focused bool
// decoHeight is the height of the fallback decoration for platforms such // decoHeight is the height of the fallback decoration for platforms such
// as Wayland that may need fallback client-side decorations. // as Wayland that may need fallback client-side decorations.
decoHeight unit.Dp decoHeight unit.Dp
@@ -131,8 +135,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
} }
@@ -147,9 +173,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)
@@ -160,23 +190,29 @@ 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)
// Wakeup wakes up the event loop and sends a WakeupEvent. // Wakeup wakes up the event loop and sends a WakeupEvent.
Wakeup() // 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 {
@@ -186,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 {
@@ -221,3 +362,6 @@ func walkActions(actions system.Action, do func(system.Action)) {
} }
} }
} }
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
+145 -102
View File
@@ -123,31 +123,36 @@ 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/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/io/transfer"
"gioui.org/unit" "gioui.org/unit"
) )
type window struct { type window struct {
callbacks *callbacks callbacks *callbacks
loop *eventLoop
view C.jobject view C.jobject
handle cgo.Handle handle cgo.Handle
@@ -156,18 +161,19 @@ type window struct {
fontScale float32 fontScale float32
insets pixelInsets 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
} }
} }
@@ -199,9 +205,9 @@ type pixelInsets struct {
top, bottom, left, right int top, bottom, left, right int
} }
// ViewEvent is sent whenever the Window's underlying Android view // AndroidViewEvent is sent whenever the Window's underlying Android view
// changes. // 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.
@@ -485,24 +491,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)
} }
@@ -516,7 +528,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
@@ -532,7 +544,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
@@ -554,9 +566,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.StageInactive { w.draw(env, true)
w.draw(env, true)
}
} }
//export Java_org_gioui_GioView_onFrameCallback //export Java_org_gioui_GioView_onFrameCallback
@@ -565,10 +575,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
if !exist { if !exist {
return return
} }
if w.stage < system.StageInactive { if w.visible && w.animating {
return
}
if w.animating {
w.draw(env, false) w.draw(env, false)
callVoidMethod(env, w.view, gioView.postFrameCallback) callVoidMethod(env, w.view, gioView.postFrameCallback)
} }
@@ -577,7 +584,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
//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)
if w.callbacks.Event(key.Event{Name: key.NameBack}) { if w.processEvent(key.Event{Name: key.NameBack}) {
return C.JNI_TRUE return C.JNI_TRUE
} }
return C.JNI_FALSE return C.JNI_FALSE
@@ -586,7 +593,8 @@ 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
@@ -598,9 +606,7 @@ func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C
left: int(left), left: int(left),
right: int(right), right: int(right),
} }
if w.stage >= system.StageInactive { w.draw(env, true)
w.draw(env, true)
}
} }
//export Java_org_gioui_GioView_initializeAccessibilityNodeInfo //export Java_org_gioui_GioView_initializeAccessibilityNodeInfo
@@ -661,7 +667,35 @@ 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 { 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 {
@@ -704,7 +738,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
@@ -749,25 +783,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
@@ -778,18 +810,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")
@@ -826,10 +850,13 @@ 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
@@ -837,14 +864,14 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
const inchPrDp = 1.0 / 160 const inchPrDp = 1.0 / 160
ppdp := float32(w.dpi) * inchPrDp ppdp := float32(w.dpi) * inchPrDp
dppp := unit.Dp(1.0 / ppdp) dppp := unit.Dp(1.0 / ppdp)
insets := system.Insets{ insets := Insets{
Top: unit.Dp(w.insets.top) * dppp, Top: unit.Dp(w.insets.top) * dppp,
Bottom: unit.Dp(w.insets.bottom) * dppp, Bottom: unit.Dp(w.insets.bottom) * dppp,
Left: unit.Dp(w.insets.left) * dppp, Left: unit.Dp(w.insets.left) * dppp,
Right: unit.Dp(w.insets.right) * dppp, Right: unit.Dp(w.insets.right) * dppp,
} }
w.callbacks.Event(frameEvent{ w.processEvent(frameEvent{
FrameEvent: system.FrameEvent{ FrameEvent: FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Insets: insets, Insets: insets,
@@ -898,8 +925,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
@@ -943,7 +970,7 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
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)))
@@ -953,18 +980,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
} }
@@ -993,8 +1020,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),
@@ -1145,6 +1172,8 @@ 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
@@ -1291,14 +1320,14 @@ func findClass(env *C.JNIEnv, name string) C.jclass {
func osMain() { func osMain() {
} }
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))
}) })
@@ -1312,47 +1341,56 @@ 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
}
w.callbacks.Event(ConfigEvent{Config: w.config})
}) })
} }
func (w *window) setConfig(env *C.JNIEnv, cnf Config) {
prev := w.config
// Decorations are never disabled.
cnf.Decorated = true
if prev.Orientation != cnf.Orientation {
w.config.Orientation = cnf.Orientation
setOrientation(env, w.view, cnf.Orientation)
}
if prev.NavigationColor != cnf.NavigationColor {
w.config.NavigationColor = cnf.NavigationColor
setNavigationColor(env, w.view, cnf.NavigationColor)
}
if prev.StatusColor != cnf.StatusColor {
w.config.StatusColor = cnf.StatusColor
setStatusColor(env, w.view, cnf.StatusColor)
}
if prev.Mode != cnf.Mode {
switch cnf.Mode {
case Fullscreen:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE)
w.config.Mode = Fullscreen
case Windowed:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE)
w.config.Mode = Windowed
}
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.processEvent(ConfigEvent{Config: w.config})
}
func (w *window) Perform(system.Action) {} func (w *window) Perform(system.Action) {}
func (w *window) SetCursor(cursor pointer.Cursor) { func (w *window) SetCursor(cursor pointer.Cursor) {
@@ -1361,9 +1399,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()
}) })
} }
@@ -1454,4 +1493,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{})
}
+27 -15
View File
@@ -6,7 +6,7 @@ package app
#include <Foundation/Foundation.h> #include <Foundation/Foundation.h>
__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void); __attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
__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);
@@ -42,7 +42,7 @@ static CFTypeRef newNSString(unichar *chars, NSUInteger length) {
import "C" import "C"
import ( import (
"errors" "errors"
"runtime/cgo" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"unicode/utf16" "unicode/utf16"
@@ -70,11 +70,18 @@ type displayLink struct {
running uint32 running uint32
} }
// displayLinks maps CFTypeRefs to *displayLinks.
var displayLinks sync.Map
var mainFuncs = make(chan func(), 1) var mainFuncs = make(chan func(), 1)
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
} }
@@ -121,25 +128,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 +207,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 +264,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()
}) })
} }
+160 -73
View File
@@ -12,6 +12,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 +22,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 +31,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 +81,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 +110,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,55 +128,59 @@ 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
m := unit.Metric{ m := unit.Metric{
PxPerDp: float32(params.dpi) * inchPrDp, PxPerDp: float32(params.dpi) * inchPrDp,
PxPerSp: float32(params.sdpi) * inchPrDp, PxPerSp: float32(params.sdpi) * inchPrDp,
} }
dppp := unit.Dp(1. / m.PxPerDp) dppp := unit.Dp(1. / m.PxPerDp)
w.w.Event(frameEvent{ w.ProcessEvent(frameEvent{
FrameEvent: system.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.Dp(params.top) * dppp, Top: unit.Dp(params.top) * dppp,
Bottom: unit.Dp(params.bottom) * dppp, Bottom: unit.Dp(params.bottom) * dppp,
Left: unit.Dp(params.left) * dppp, Left: unit.Dp(params.left) * dppp,
@@ -175,26 +193,34 @@ func (w *window) draw(sync bool) {
} }
//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
@@ -204,56 +230,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,
@@ -265,11 +291,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]))
@@ -280,7 +311,7 @@ func (w *window) WriteClipboard(s string) {
func (w *window) Configure([]Option) { func (w *window) Configure([]Option) {
// Decorations are never disabled. // Decorations are never disabled.
w.config.Decorated = true w.config.Decorated = true
w.w.Event(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
} }
func (w *window) EditorStateChanged(old, new editorState) {} func (w *window) EditorStateChanged(old, new editorState) {}
@@ -288,10 +319,6 @@ func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) Perform(system.Action) {} func (w *window) Perform(system.Action) {}
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 {
@@ -303,8 +330,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,
}) })
} }
@@ -343,17 +370,77 @@ func (w *window) ShowTextInput(show bool) {
func (w *window) SetInputHint(_ key.InputHint) {} func (w *window) SetInputHint(_ key.InputHint) {}
func newWindow(win *callbacks, options []Option) error { func (w *window) ProcessEvent(e event.Event) {
mainWindow.in <- windowAndConfig{win, options} w.w.ProcessEvent(e)
return <-mainWindow.errs w.loop.FlushEvents()
} }
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func newWindow(win *callbacks, options []Option) {
mainWindow.in <- windowAndConfig{win, options}
<-mainWindow.windows
}
var mainMode = mainModeUndefined
const (
mainModeUndefined = iota
mainModeExe
mainModeLibrary
)
func osMain() { func osMain() {
if !isMainThread() {
panic("app.Main must be run on the main goroutine")
}
switch mainMode {
case mainModeUndefined:
mainMode = mainModeExe
var argv []*C.char
for _, arg := range os.Args {
a := C.CString(arg)
defer C.free(unsafe.Pointer(a))
argv = append(argv, a)
}
C.gio_applicationMain(C.int(len(argv)), unsafe.SliceData(argv))
case mainModeExe:
panic("app.Main may be called only once")
case mainModeLibrary:
// Do nothing, we're embedded as a library.
}
} }
//export gio_runMain //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{})
}
+56 -39
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,13 +120,16 @@ 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();
} }
@@ -148,13 +155,13 @@ NSArray<UIKeyCommand *> *_keyCommands;
- (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 +182,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 +194,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 +234,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 +275,28 @@ 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;
}
@end
int gio_applicationMain(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([_gioAppDelegate class]));
}
}
+104 -101
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,18 @@ 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 Element js.Value
} }
@@ -53,9 +56,6 @@ type window struct {
composing bool composing bool
requestFocus bool requestFocus bool
chanAnimation chan struct{}
chanRedraw chan struct{}
config Config config Config
inset f32.Point inset f32.Point
scale float32 scale float32
@@ -68,7 +68,7 @@ type window struct {
contextStatus contextStatus 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)
@@ -83,7 +83,9 @@ 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,
} }
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")
@@ -93,42 +95,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(ViewEvent{Element: cont})
w.w.Event(system.StageEvent{Stage: system.StageRunning})
w.resize()
w.draw(true)
for {
select {
case <-w.wakeups:
w.w.Event(wakeupEvent{})
case <-w.chanAnimation:
w.animCallback()
case <-w.chanRedraw:
w.draw(true)
}
}
}()
return nil
} }
func getContainer(doc js.Value) js.Value { func getContainer(doc js.Value) js.Value {
@@ -188,12 +176,12 @@ func (w *window) addEventListeners() {
w.cnv.Set("width", 0) w.cnv.Set("width", 0)
w.cnv.Set("height", 0) w.cnv.Set("height", 0)
w.resize() w.resize()
w.requestRedraw() w.draw(true)
return nil 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.requestRedraw() 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{} {
@@ -201,22 +189,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{} {
if w.w.Event(key.Event{Name: key.NameBack}) { if w.processEvent(key.Event{Name: key.NameBack}) {
return w.browserHistory.Call("forward") return w.browserHistory.Call("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
@@ -274,18 +251,20 @@ 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}) w.config.Focused = false
w.processEvent(ConfigEvent{Config: w.config})
w.blur() w.blur()
return nil return nil
}) })
@@ -374,10 +353,50 @@ func (w *window) keyEvent(e js.Value, ks key.State) {
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 {
@@ -398,7 +417,7 @@ func modifiersFor(e js.Value) key.Modifiers {
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")
@@ -425,8 +444,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,
@@ -448,7 +467,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")
@@ -475,8 +494,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,
@@ -502,17 +521,6 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
return jsf return jsf
} }
func (w *window) animCallback() {
anim := w.animating
w.animRequested = anim
if anim {
w.requestAnimationFrame.Invoke(w.redraw)
}
if anim {
w.draw(false)
}
}
func (w *window) EditorStateChanged(old, new editorState) {} func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) SetAnimating(anim bool) { func (w *window) SetAnimating(anim bool) {
@@ -533,14 +541,14 @@ func (w *window) ReadClipboard() {
w.clipboard.Call("readText", w.clipboard).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) {
@@ -568,7 +576,7 @@ 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
} }
w.w.Event(ConfigEvent{Config: w.config}) w.processEvent(ConfigEvent{Config: w.config})
} }
func (w *window) Perform(system.Action) {} func (w *window) Perform(system.Action) {}
@@ -607,23 +615,14 @@ 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 { w.blur()
w.blur() }
}
}()
} }
func (w *window) SetInputHint(mode key.InputHint) { func (w *window) SetInputHint(mode key.InputHint) {
@@ -640,7 +639,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() {
@@ -660,13 +659,20 @@ func (w *window) draw(sync bool) {
if w.contextStatus == contextStatusLost { if w.contextStatus == contextStatusLost {
return 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{ w.processEvent(frameEvent{
FrameEvent: system.FrameEvent{ FrameEvent: FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: size, Size: size,
Insets: insets, Insets: insets,
@@ -676,10 +682,10 @@ func (w *window) draw(sync bool) {
}) })
} }
func (w *window) getConfig() (image.Point, system.Insets, unit.Metric) { func (w *window) getConfig() (image.Point, Insets, unit.Metric) {
invscale := unit.Dp(1. / w.scale) 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.Dp(w.inset.Y) * invscale, Bottom: unit.Dp(w.inset.Y) * invscale,
Right: unit.Dp(w.inset.X) * invscale, Right: unit.Dp(w.inset.X) * invscale,
}, unit.Metric{ }, unit.Metric{
@@ -735,19 +741,12 @@ func (w *window) navigationColor(c color.NRGBA) {
theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B})) theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
} }
func (w *window) requestRedraw() {
select {
case w.chanRedraw <- struct{}{}:
default:
}
}
func osMain() { 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":
@@ -814,11 +813,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())
}
+305 -218
View File
@@ -8,16 +8,21 @@ package app
import ( import (
"errors" "errors"
"image" "image"
"io"
"runtime" "runtime"
"runtime/cgo"
"strings"
"time" "time"
"unicode" "unicode"
"unicode/utf8" "unicode/utf8"
"gioui.org/internal/f32" "gioui.org/internal/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"
_ "gioui.org/internal/cocoainit" _ "gioui.org/internal/cocoainit"
@@ -35,8 +40,9 @@ import (
#define MOUSE_SCROLL 4 #define MOUSE_SCROLL 4
__attribute__ ((visibility ("hidden"))) void gio_main(void); __attribute__ ((visibility ("hidden"))) void gio_main(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
static void writeClipboard(CFTypeRef str) { static void writeClipboard(CFTypeRef str) {
@autoreleasepool { @autoreleasepool {
@@ -56,144 +62,212 @@ static CFTypeRef readClipboard(void) {
} }
static CGFloat viewHeight(CFTypeRef viewRef) { static CGFloat viewHeight(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef; @autoreleasepool {
return [view bounds].size.height; NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.height;
}
} }
static CGFloat viewWidth(CFTypeRef viewRef) { static CGFloat viewWidth(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef; @autoreleasepool {
return [view bounds].size.width; NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.width;
}
} }
static CGFloat getScreenBackingScale(void) { static CGFloat getScreenBackingScale(void) {
return [NSScreen.mainScreen backingScaleFactor]; @autoreleasepool {
return [NSScreen.mainScreen backingScaleFactor];
}
} }
static CGFloat getViewBackingScale(CFTypeRef viewRef) { static CGFloat getViewBackingScale(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef; @autoreleasepool {
return [view.window backingScaleFactor]; NSView *view = (__bridge NSView *)viewRef;
return [view.window backingScaleFactor];
}
} }
static void setNeedsDisplay(CFTypeRef viewRef) { static void setNeedsDisplay(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef; @autoreleasepool {
[view setNeedsDisplay:YES]; NSView *view = (__bridge NSView *)viewRef;
[view setNeedsDisplay:YES];
}
} }
static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) { static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
return [window cascadeTopLeftFromPoint:topLeft]; NSWindow *window = (__bridge NSWindow *)windowRef;
return [window cascadeTopLeftFromPoint:topLeft];
}
} }
static void makeKeyAndOrderFront(CFTypeRef windowRef) { static void makeKeyAndOrderFront(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
[window makeKeyAndOrderFront:nil]; NSWindow *window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
}
} }
static void toggleFullScreen(CFTypeRef windowRef) { static void toggleFullScreen(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
[window toggleFullScreen:nil]; NSWindow *window = (__bridge NSWindow *)windowRef;
[window toggleFullScreen:nil];
}
} }
static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) { static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
return [window styleMask]; NSWindow *window = (__bridge NSWindow *)windowRef;
return [window styleMask];
}
} }
static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) { static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
window.styleMask = mask; NSWindow *window = (__bridge NSWindow *)windowRef;
window.styleMask = mask;
}
} }
static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) { static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
window.titleVisibility = state; NSWindow *window = (__bridge NSWindow *)windowRef;
window.titleVisibility = state;
}
} }
static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) { static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
window.titlebarAppearsTransparent = (BOOL)transparent; NSWindow *window = (__bridge NSWindow *)windowRef;
window.titlebarAppearsTransparent = (BOOL)transparent;
}
} }
static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) { static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
[window standardWindowButton:btn].hidden = (BOOL)hide; NSWindow *window = (__bridge NSWindow *)windowRef;
[window standardWindowButton:btn].hidden = (BOOL)hide;
}
} }
static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) { static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
[window performWindowDragWithEvent:(__bridge NSEvent*)evt]; NSWindow *window = (__bridge NSWindow *)windowRef;
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
}
} }
static void closeWindow(CFTypeRef windowRef) { static void closeWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef; @autoreleasepool {
[window performClose:nil]; NSWindow* window = (__bridge NSWindow *)windowRef;
[window performClose:nil];
}
} }
static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
NSWindow* window = (__bridge NSWindow *)windowRef; @autoreleasepool {
NSSize size = NSMakeSize(width, height); NSWindow* window = (__bridge NSWindow *)windowRef;
[window setContentSize:size]; NSSize size = NSMakeSize(width, height);
[window setContentSize:size];
}
} }
static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
NSWindow* window = (__bridge NSWindow *)windowRef; @autoreleasepool {
window.contentMinSize = NSMakeSize(width, height); NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMinSize = NSMakeSize(width, height);
}
} }
static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) { static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
NSWindow* window = (__bridge NSWindow *)windowRef; @autoreleasepool {
window.contentMaxSize = NSMakeSize(width, height); NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMaxSize = NSMakeSize(width, height);
}
} }
static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) { static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
NSWindow* window = (__bridge NSWindow *)windowRef; @autoreleasepool {
NSRect r = NSMakeRect(x, y, w, h); NSWindow* window = (__bridge NSWindow *)windowRef;
[window setFrame:r display:YES]; NSRect r = NSMakeRect(x, y, w, h);
[window setFrame:r display:YES];
}
}
static void resetLayerFrame(CFTypeRef viewRef) {
@autoreleasepool {
NSView* view = (__bridge NSView *)viewRef;
NSRect r = view.frame;
view.layer.frame = r;
}
} }
static void hideWindow(CFTypeRef windowRef) { static void hideWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef; @autoreleasepool {
[window miniaturize:window]; NSWindow* window = (__bridge NSWindow *)windowRef;
[window miniaturize:window];
}
} }
static void unhideWindow(CFTypeRef windowRef) { static void unhideWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef; @autoreleasepool {
[window deminiaturize:window]; NSWindow* window = (__bridge NSWindow *)windowRef;
[window deminiaturize:window];
}
} }
static NSRect getScreenFrame(CFTypeRef windowRef) { static NSRect getScreenFrame(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef; @autoreleasepool {
return [[window screen] frame]; NSWindow* window = (__bridge NSWindow *)windowRef;
return [[window screen] frame];
}
} }
static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) { static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
window.title = (__bridge NSString *)titleRef; NSWindow *window = (__bridge NSWindow *)windowRef;
window.title = (__bridge NSString *)titleRef;
}
} }
static int isWindowZoomed(CFTypeRef windowRef) { static int isWindowZoomed(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
return window.zoomed ? 1 : 0; NSWindow *window = (__bridge NSWindow *)windowRef;
return window.zoomed ? 1 : 0;
}
} }
static void zoomWindow(CFTypeRef windowRef) { static void zoomWindow(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef; @autoreleasepool {
[window zoom:nil]; NSWindow *window = (__bridge NSWindow *)windowRef;
[window zoom:nil];
}
} }
static CFTypeRef layerForView(CFTypeRef viewRef) { static CFTypeRef layerForView(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef; @autoreleasepool {
return (__bridge CFTypeRef)view.layer; NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.layer;
}
} }
static CFTypeRef windowForView(CFTypeRef viewRef) { static CFTypeRef windowForView(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef; @autoreleasepool {
return (__bridge CFTypeRef)view.window; NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.window;
}
} }
static void raiseWindow(CFTypeRef windowRef) { static void raiseWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef; @autoreleasepool {
[window makeKeyAndOrderFront:nil]; NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
if (![currentApp isActive]) {
[currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
}
} }
static CFTypeRef createInputContext(CFTypeRef clientRef) { static CFTypeRef createInputContext(CFTypeRef clientRef) {
@@ -205,23 +279,23 @@ static CFTypeRef createInputContext(CFTypeRef clientRef) {
} }
static void discardMarkedText(CFTypeRef viewRef) { static void discardMarkedText(CFTypeRef viewRef) {
@autoreleasepool { @autoreleasepool {
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef; id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
NSTextInputContext *ctx = [NSTextInputContext currentInputContext]; NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
if (view == [ctx client]) { if (view == [ctx client]) {
[ctx discardMarkedText]; [ctx discardMarkedText];
} }
} }
} }
static void invalidateCharacterCoordinates(CFTypeRef viewRef) { static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
@autoreleasepool { @autoreleasepool {
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef; id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
NSTextInputContext *ctx = [NSTextInputContext currentInputContext]; NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
if (view == [ctx client]) { if (view == [ctx client]) {
[ctx invalidateCharacterCoordinates]; [ctx invalidateCharacterCoordinates];
} }
} }
} }
*/ */
import "C" import "C"
@@ -231,9 +305,9 @@ func init() {
runtime.LockOSThread() runtime.LockOSThread()
} }
// ViewEvent notified the client of changes to the window AppKit handles. // AppKitViewEvent notifies the client of changes to the window AppKit handles.
// The handles are retained until another ViewEvent is sent. // The handles are retained until another AppKitViewEvent is sent.
type ViewEvent struct { type AppKitViewEvent struct {
// View is a CFTypeRef for the NSView for the window. // View is a CFTypeRef for the NSView for the window.
View uintptr View uintptr
// Layer is a CFTypeRef of the CALayer of View. // Layer is a CFTypeRef of the CALayer of View.
@@ -243,21 +317,20 @@ type ViewEvent struct {
type window struct { type window struct {
view C.CFTypeRef view C.CFTypeRef
w *callbacks w *callbacks
stage system.Stage anim bool
visible bool
displayLink *displayLink displayLink *displayLink
// redraw is a single entry channel for making sure only one // redraw is a single entry channel for making sure only one
// display link redraw request is in flight. // display link redraw request is in flight.
redraw chan struct{} redraw chan struct{}
cursor pointer.Cursor cursor pointer.Cursor
pointerBtns pointer.Buttons pointerBtns pointer.Buttons
loop *eventLoop
scale float32 scale float32
config Config config Config
} }
// viewMap is the mapping from Cocoa NSViews to Go windows.
var viewMap = make(map[C.CFTypeRef]*window)
// launched is closed when applicationDidFinishLaunching is called. // launched is closed when applicationDidFinishLaunching is called.
var launched = make(chan struct{}) var launched = make(chan struct{})
@@ -265,30 +338,8 @@ var launched = make(chan struct{})
// cascadeTopLeftFromPoint. // cascadeTopLeftFromPoint.
var nextTopLeft C.NSPoint var nextTopLeft C.NSPoint
// mustView is like lookupView, except that it panics func windowFor(h C.uintptr_t) *window {
// if the view isn't mapped. return cgo.Handle(h).Value().(*window)
func mustView(view C.CFTypeRef) *window {
w, ok := lookupView(view)
if !ok {
panic("no window for view")
}
return w
}
func lookupView(view C.CFTypeRef) (*window, bool) {
w, exists := viewMap[view]
if !exists {
return nil, false
}
return w, true
}
func deleteView(view C.CFTypeRef) {
delete(viewMap, view)
}
func insertView(view C.CFTypeRef, w *window) {
viewMap[view] = w
} }
func (w *window) contextView() C.CFTypeRef { func (w *window) contextView() C.CFTypeRef {
@@ -297,13 +348,20 @@ func (w *window) contextView() C.CFTypeRef {
func (w *window) ReadClipboard() { func (w *window) ReadClipboard() {
cstr := C.readClipboard() cstr := C.readClipboard()
defer C.CFRelease(cstr) if cstr != 0 {
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) {
cstr := stringToNSString(s) cstr := stringToNSString(string(s))
defer C.CFRelease(cstr) defer C.CFRelease(cstr)
C.writeClipboard(cstr) C.writeClipboard(cstr)
} }
@@ -365,11 +423,11 @@ func (w *window) Configure(options []Option) {
case Minimized: case Minimized:
C.unhideWindow(window) C.unhideWindow(window)
case Maximized: case Maximized:
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
} }
w.config.Mode = Windowed w.config.Mode = Windowed
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
w.setTitle(prev, cnf) w.setTitle(prev, cnf)
if prev.Size != cnf.Size { if prev.Size != cnf.Size {
w.config.Size = cnf.Size w.config.Size = cnf.Size
@@ -406,8 +464,11 @@ func (w *window) Configure(options []Option) {
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans) C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans) C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans) C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
// When toggling the titlebar, the layer doesn't update its frame
// until the next resize. Force it.
C.resetLayerFrame(w.view)
} }
w.w.Event(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
} }
func (w *window) setTitle(prev, cnf Config) { func (w *window) setTitle(prev, cnf Config) {
@@ -458,7 +519,8 @@ func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {} func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) SetAnimating(anim bool) { func (w *window) SetAnimating(anim bool) {
if anim { w.anim = anim
if w.anim && w.visible {
w.displayLink.Start() w.displayLink.Start()
} else { } else {
w.displayLink.Stop() w.displayLink.Stop()
@@ -475,26 +537,18 @@ func (w *window) runOnMain(f func()) {
}) })
} }
func (w *window) setStage(stage system.Stage) {
if stage == w.stage {
return
}
w.stage = stage
w.w.Event(system.StageEvent{Stage: stage})
}
//export gio_onKeys //export gio_onKeys
func gio_onKeys(view, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) { func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
str := nsstringToString(cstr) str := nsstringToString(cstr)
kmods := convertMods(mods) kmods := convertMods(mods)
ks := key.Release ks := key.Release
if keyDown { if keyDown {
ks = key.Press ks = key.Press
} }
w := mustView(view) w := windowFor(h)
for _, k := range str { for _, k := range str {
if n, ok := convertKey(k); ok { if n, ok := convertKey(k); ok {
w.w.Event(key.Event{ w.ProcessEvent(key.Event{
Name: n, Name: n,
Modifiers: kmods, Modifiers: kmods,
State: ks, State: ks,
@@ -504,15 +558,15 @@ func gio_onKeys(view, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown
} }
//export gio_onText //export gio_onText
func gio_onText(view, cstr C.CFTypeRef) { func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) {
str := nsstringToString(cstr) str := nsstringToString(cstr)
w := mustView(view) w := windowFor(h)
w.w.EditorInsert(str) w.w.EditorInsert(str)
} }
//export gio_onMouse //export gio_onMouse
func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) { func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
w := mustView(view) w := windowFor(h)
t := time.Duration(float64(ti)*float64(time.Second) + .5) t := time.Duration(float64(ti)*float64(time.Second) + .5)
xf, yf := float32(x)*w.scale, float32(y)*w.scale xf, yf := float32(x)*w.scale, float32(y)*w.scale
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
@@ -523,8 +577,10 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
btn = pointer.ButtonPrimary btn = pointer.ButtonPrimary
case 1: case 1:
btn = pointer.ButtonSecondary btn = pointer.ButtonSecondary
case 2:
btn = pointer.ButtonTertiary
} }
var typ pointer.Type var typ pointer.Kind
switch cdir { switch cdir {
case C.MOUSE_MOVE: case C.MOUSE_MOVE:
typ = pointer.Move typ = pointer.Move
@@ -547,8 +603,8 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
default: default:
panic("invalid direction") panic("invalid direction")
} }
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: typ, Kind: typ,
Source: pointer.Mouse, Source: pointer.Mouse,
Time: t, Time: t,
Buttons: w.pointerBtns, Buttons: w.pointerBtns,
@@ -559,35 +615,29 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
} }
//export gio_onDraw //export gio_onDraw
func gio_onDraw(view C.CFTypeRef) { func gio_onDraw(h C.uintptr_t) {
w := mustView(view) w := windowFor(h)
w.draw() w.draw()
} }
//export gio_onFocus //export gio_onFocus
func gio_onFocus(view C.CFTypeRef, focus C.int) { func gio_onFocus(h C.uintptr_t, focus C.int) {
w := mustView(view) w := windowFor(h)
w.w.Event(key.FocusEvent{Focus: focus == 1})
if w.stage >= system.StageInactive {
if focus == 0 {
w.setStage(system.StageInactive)
} else {
w.setStage(system.StageRunning)
}
}
w.SetCursor(w.cursor) w.SetCursor(w.cursor)
w.config.Focused = focus == 1
w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export gio_onChangeScreen //export gio_onChangeScreen
func gio_onChangeScreen(view C.CFTypeRef, did uint64) { func gio_onChangeScreen(h C.uintptr_t, did uint64) {
w := mustView(view) w := windowFor(h)
w.displayLink.SetDisplayID(did) w.displayLink.SetDisplayID(did)
C.setNeedsDisplay(w.view) C.setNeedsDisplay(w.view)
} }
//export gio_hasMarkedText //export gio_hasMarkedText
func gio_hasMarkedText(view C.CFTypeRef) C.int { func gio_hasMarkedText(h C.uintptr_t) C.int {
w := mustView(view) w := windowFor(h)
state := w.w.EditorState() state := w.w.EditorState()
if state.compose.Start != -1 { if state.compose.Start != -1 {
return 1 return 1
@@ -596,8 +646,8 @@ func gio_hasMarkedText(view C.CFTypeRef) C.int {
} }
//export gio_markedRange //export gio_markedRange
func gio_markedRange(view C.CFTypeRef) C.NSRange { func gio_markedRange(h C.uintptr_t) C.NSRange {
w := mustView(view) w := windowFor(h)
state := w.w.EditorState() state := w.w.EditorState()
rng := state.compose rng := state.compose
start, end := rng.Start, rng.End start, end := rng.Start, rng.End
@@ -612,8 +662,8 @@ func gio_markedRange(view C.CFTypeRef) C.NSRange {
} }
//export gio_selectedRange //export gio_selectedRange
func gio_selectedRange(view C.CFTypeRef) C.NSRange { func gio_selectedRange(h C.uintptr_t) C.NSRange {
w := mustView(view) w := windowFor(h)
state := w.w.EditorState() state := w.w.EditorState()
rng := state.Selection rng := state.Selection
start, end := rng.Start, rng.End start, end := rng.Start, rng.End
@@ -628,14 +678,14 @@ func gio_selectedRange(view C.CFTypeRef) C.NSRange {
} }
//export gio_unmarkText //export gio_unmarkText
func gio_unmarkText(view C.CFTypeRef) { func gio_unmarkText(h C.uintptr_t) {
w := mustView(view) w := windowFor(h)
w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
} }
//export gio_setMarkedText //export gio_setMarkedText
func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) { func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
w := mustView(view) w := windowFor(h)
str := nsstringToString(cstr) str := nsstringToString(cstr)
state := w.w.EditorState() state := w.w.EditorState()
rng := state.compose rng := state.compose
@@ -674,8 +724,8 @@ func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange
} }
//export gio_substringForProposedRange //export gio_substringForProposedRange
func gio_substringForProposedRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef { func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
w := mustView(view) w := windowFor(h)
state := w.w.EditorState() state := w.w.EditorState()
start, end := state.Snippet.Start, state.Snippet.End start, end := state.Snippet.Start, state.Snippet.End
if start > end { if start > end {
@@ -695,8 +745,8 @@ func gio_substringForProposedRange(view C.CFTypeRef, crng C.NSRange, actual C.NS
} }
//export gio_insertText //export gio_insertText
func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) { func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
w := mustView(view) w := windowFor(h)
state := w.w.EditorState() state := w.w.EditorState()
rng := state.compose rng := state.compose
if rng.Start == -1 { if rng.Start == -1 {
@@ -720,13 +770,13 @@ func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) {
} }
//export gio_characterIndexForPoint //export gio_characterIndexForPoint
func gio_characterIndexForPoint(view C.CFTypeRef, p C.NSPoint) C.NSUInteger { func gio_characterIndexForPoint(h C.uintptr_t, p C.NSPoint) C.NSUInteger {
return C.NSNotFound return C.NSNotFound
} }
//export gio_firstRectForCharacterRange //export gio_firstRectForCharacterRange
func gio_firstRectForCharacterRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.NSRect { func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
w := mustView(view) w := windowFor(h)
state := w.w.EditorState() state := w.w.EditorState()
sel := state.Selection sel := state.Selection
u16start := state.UTF16Index(sel.Start) u16start := state.UTF16Index(sel.Start)
@@ -753,6 +803,10 @@ func (w *window) draw() {
case <-w.redraw: case <-w.redraw:
default: default:
} }
w.visible = true
if w.anim {
w.SetAnimating(w.anim)
}
w.scale = float32(C.getViewBackingScale(w.view)) w.scale = float32(C.getViewBackingScale(w.view))
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view)) wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
sz := image.Point{ sz := image.Point{
@@ -761,15 +815,14 @@ func (w *window) draw() {
} }
if sz != w.config.Size { if 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})
} }
if sz.X == 0 || sz.Y == 0 { if sz.X == 0 || sz.Y == 0 {
return return
} }
cfg := configFor(w.scale) cfg := configFor(w.scale)
w.setStage(system.StageRunning) w.ProcessEvent(frameEvent{
w.w.Event(frameEvent{ FrameEvent: FrameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Metric: cfg, Metric: cfg,
@@ -778,6 +831,27 @@ func (w *window) draw() {
}) })
} }
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.loop.FlushEvents()
}
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func configFor(scale float32) unit.Metric { func configFor(scale float32) unit.Metric {
return unit.Metric{ return unit.Metric{
PxPerDp: scale, PxPerDp: scale,
@@ -785,56 +859,54 @@ func configFor(scale float32) unit.Metric {
} }
} }
//export gio_onClose //export gio_onAttached
func gio_onClose(view C.CFTypeRef) { func gio_onAttached(h C.uintptr_t, attached C.int) {
w := mustView(view) w := windowFor(h)
if attached != 0 {
layer := C.layerForView(w.view)
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
} else {
w.ProcessEvent(AppKitViewEvent{})
w.visible = false
w.SetAnimating(w.anim)
}
}
//export gio_onDestroy
func gio_onDestroy(h C.uintptr_t) {
w := windowFor(h)
w.ProcessEvent(DestroyEvent{})
w.displayLink.Close() w.displayLink.Close()
w.w.Event(ViewEvent{})
deleteView(view)
w.w.Event(system.DestroyEvent{})
C.CFRelease(w.view)
w.view = 0
w.displayLink = nil w.displayLink = nil
cgo.Handle(h).Delete()
w.view = 0
} }
//export gio_onHide //export gio_onHide
func gio_onHide(view C.CFTypeRef) { func gio_onHide(h C.uintptr_t) {
w := mustView(view) w := windowFor(h)
w.setStage(system.StagePaused) w.visible = false
w.SetAnimating(w.anim)
} }
//export gio_onShow //export gio_onShow
func gio_onShow(view C.CFTypeRef) { func gio_onShow(h C.uintptr_t) {
w := mustView(view) w := windowFor(h)
w.setStage(system.StageRunning) w.draw()
} }
//export gio_onFullscreen //export gio_onFullscreen
func gio_onFullscreen(view C.CFTypeRef) { func gio_onFullscreen(h C.uintptr_t) {
w := mustView(view) w := windowFor(h)
w.config.Mode = Fullscreen w.config.Mode = Fullscreen
w.w.Event(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export gio_onWindowed //export gio_onWindowed
func gio_onWindowed(view C.CFTypeRef) { func gio_onWindowed(h C.uintptr_t) {
w := mustView(view) w := windowFor(h)
w.config.Mode = Windowed w.config.Mode = Windowed
w.w.Event(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onAppHide
func gio_onAppHide() {
for _, w := range viewMap {
w.setStage(system.StagePaused)
}
}
//export gio_onAppShow
func gio_onAppShow() {
for _, w := range viewMap {
w.setStage(system.StageRunning)
}
} }
//export gio_onFinishLaunching //export gio_onFinishLaunching
@@ -842,20 +914,27 @@ func gio_onFinishLaunching() {
close(launched) close(launched)
} }
func newWindow(win *callbacks, options []Option) error { func newWindow(win *callbacks, options []Option) {
<-launched <-launched
errch := make(chan error) res := make(chan struct{})
runOnMain(func() { runOnMain(func() {
w, err := newOSWindow() w := &window{
if err != nil { redraw: make(chan struct{}, 1),
errch <- err w: win,
}
w.loop = newEventLoop(w.w, w.wakeup)
win.SetDriver(w)
res <- struct{}{}
var cnf Config
cnf.apply(unit.Metric{}, options)
if err := w.init(cnf.CustomRenderer); err != nil {
w.ProcessEvent(DestroyEvent{Err: err})
return return
} }
errch <- nil
w.w = win
window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0) window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0)
// Release our reference now that the NSWindow has it.
C.CFRelease(w.view)
w.updateWindowMode() w.updateWindowMode()
win.SetDriver(w)
w.Configure(options) w.Configure(options)
if nextTopLeft.x == 0 && nextTopLeft.y == 0 { if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
// cascadeTopLeftFromPoint treats (0, 0) as a no-op, // cascadeTopLeftFromPoint treats (0, 0) as a no-op,
@@ -865,48 +944,52 @@ func newWindow(win *callbacks, options []Option) error {
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft) nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
// makeKeyAndOrderFront assumes ownership of our window reference. // makeKeyAndOrderFront assumes ownership of our window reference.
C.makeKeyAndOrderFront(window) C.makeKeyAndOrderFront(window)
layer := C.layerForView(w.view)
w.w.Event(ViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
}) })
return <-errch <-res
} }
func newOSWindow() (*window, error) { func (w *window) init(customRenderer bool) error {
view := C.gio_createView() presentWithTrans := 1
if customRenderer {
presentWithTrans = 0
}
view := C.gio_createView(C.int(presentWithTrans))
if view == 0 { if view == 0 {
return nil, errors.New("newOSWindows: failed to create view") return errors.New("newOSWindow: failed to create view")
} }
scale := float32(C.getViewBackingScale(view)) scale := float32(C.getViewBackingScale(view))
w := &window{ w.scale = scale
view: view, dl, err := newDisplayLink(func() {
scale: scale,
redraw: make(chan struct{}, 1),
}
dl, err := NewDisplayLink(func() {
select { select {
case w.redraw <- struct{}{}: case w.redraw <- struct{}{}:
default: default:
return return
} }
w.runOnMain(func() { w.runOnMain(func() {
C.setNeedsDisplay(w.view) if w.visible {
C.setNeedsDisplay(w.view)
}
}) })
}) })
w.displayLink = dl w.displayLink = dl
if err != nil { if err != nil {
C.CFRelease(view) C.CFRelease(view)
return nil, err return err
} }
insertView(view, w) C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
return w, nil w.view = view
return nil
} }
func osMain() { func osMain() {
if !isMainThread() {
panic("app.Main must run on the main goroutine")
}
C.gio_main() C.gio_main()
} }
func convertKey(k rune) (string, bool) { func convertKey(k rune) (key.Name, bool) {
var n string var n key.Name
switch k { switch k {
case 0x1b: case 0x1b:
n = key.NameEscape n = key.NameEscape
@@ -967,7 +1050,7 @@ func convertKey(k rune) (string, bool) {
if !unicode.IsPrint(k) { if !unicode.IsPrint(k) {
return "", false return "", false
} }
n = string(k) n = key.Name(k)
} }
return n, true return n, true
} }
@@ -989,4 +1072,8 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
return kmods return kmods
} }
func (_ ViewEvent) ImplementsEvent() {} func (AppKitViewEvent) implementsViewEvent() {}
func (AppKitViewEvent) ImplementsEvent() {}
func (a AppKitViewEvent) Valid() bool {
return a != (AppKitViewEvent{})
}
+104 -62
View File
@@ -6,7 +6,7 @@
#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,40 +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_onHide(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_onShow(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_onFullscreen(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_onWindowed(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) {
gio_onFocus(view.handle, 0);
}
} }
@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.
@@ -56,12 +71,9 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
} }
// Origin is in the lower left corner. Convert to upper left. // 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, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]); gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
} }
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@end
@implementation GioView @implementation GioView
- (void)setFrameSize:(NSSize)newSize { - (void)setFrameSize:(NSSize)newSize {
[super setFrameSize:newSize]; [super setFrameSize:newSize];
@@ -70,21 +82,19 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
// drawRect is called when OpenGL is used, displayLayer otherwise. // 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 { - (void)viewDidMoveToWindow {
if (self.window == nil) { gio_onAttached(self.handle, self.window != nil ? 1 : 0);
gio_onClose((__bridge CFTypeRef)self);
}
} }
- (void)mouseDown:(NSEvent *)event { - (void)mouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0); handleMouse(self, event, MOUSE_DOWN, 0, 0);
@@ -92,24 +102,30 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
- (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)middleMouseUp:(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;
@@ -118,14 +134,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
- (void)keyDown:(NSEvent *)event { - (void)keyDown:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject: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)keys, [event timestamp], [event modifierFlags], true);
} }
- (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)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)sel {
// Don't pass commands up the responder chain. // Don't pass commands up the responder chain.
@@ -133,17 +149,17 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
} }
- (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 +171,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 +190,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_onShow(self.handle);
}
- (void)applicationDidHide:(NSNotification *)notification {
gio_onHide(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 +226,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 +267,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 +299,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 +309,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 +354,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];
@@ -362,28 +393,39 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
} }
} }
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(); gio_onFinishLaunching();
} }
- (void)applicationDidHide:(NSNotification *)aNotification {
gio_onAppHide();
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onAppShow();
}
@end @end
void gio_main() { void gio_main() {
+11 -12
View File
@@ -12,13 +12,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 +21,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 +34,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 +48,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 +56,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.
+188 -129
View File
@@ -15,6 +15,7 @@ import (
"math" "math"
"os" "os"
"os/exec" "os/exec"
"runtime"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@@ -25,10 +26,12 @@ import (
"gioui.org/app/internal/xkb" "gioui.org/app/internal/xkb"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/fling" "gioui.org/internal/fling"
"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"
) )
@@ -97,7 +100,9 @@ type wlDisplay struct {
read, write int read, write int
} }
repeat repeatState repeat repeatState
poller poller
readClipClose chan struct{}
} }
type wlSeat struct { type wlSeat struct {
@@ -137,7 +142,7 @@ type repeatState struct {
delay time.Duration delay time.Duration
key uint32 key uint32
win *callbacks win *window
stopC chan struct{} stopC chan struct{}
start time.Duration start time.Duration
@@ -194,12 +199,10 @@ type window struct {
dir f32.Point dir f32.Point
} }
stage system.Stage configured bool
dead bool
lastFrameCallback *C.struct_wl_callback lastFrameCallback *C.struct_wl_callback
animating bool animating bool
redraw bool
// The most recent configure serial waiting to be ack'ed. // The most recent configure serial waiting to be ack'ed.
serial C.uint32_t serial C.uint32_t
scale int scale int
@@ -209,9 +212,11 @@ type window struct {
wsize image.Point // window config size before going fullscreen or maximized wsize image.Point // window config size before going fullscreen or maximized
inCompositor bool // window is moving or being resized inCompositor bool // window is moving or being resized
clipReads chan clipboard.Event clipReads chan transfer.DataEvent
wakeups chan struct{} wakeups chan struct{}
closing bool
} }
type poller struct { type poller struct {
@@ -260,25 +265,17 @@ func newWLWindow(callbacks *callbacks, options []Option) error {
return err return err
} }
w.w = callbacks w.w = callbacks
go func() { w.w.SetDriver(w)
defer d.destroy()
defer w.destroy()
w.w.SetDriver(w) // Finish and commit setup from createNativeWindow.
w.Configure(options)
w.draw(true)
C.wl_surface_commit(w.surf)
// Finish and commit setup from createNativeWindow. w.ProcessEvent(WaylandViewEvent{
w.Configure(options) Display: unsafe.Pointer(w.display()),
C.wl_surface_commit(w.surf) Surface: unsafe.Pointer(w.surf),
})
w.w.Event(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
err := w.loop()
w.w.Event(WaylandViewEvent{})
w.w.Event(system.DestroyEvent{Err: err})
}()
return nil return nil
} }
@@ -354,7 +351,7 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) {
ppdp: ppdp, ppdp: ppdp,
ppsp: ppdp, ppsp: ppdp,
wakeups: make(chan struct{}, 1), wakeups: make(chan struct{}, 1),
clipReads: make(chan clipboard.Event, 1), clipReads: make(chan transfer.DataEvent, 1),
} }
w.surf = C.wl_compositor_create_surface(d.compositor) w.surf = C.wl_compositor_create_surface(d.compositor)
if w.surf == nil { if w.surf == nil {
@@ -549,15 +546,15 @@ func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) {
func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) { func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
w := callbackLoad(data).(*window) w := callbackLoad(data).(*window)
w.serial = serial w.serial = serial
w.redraw = true
C.xdg_surface_ack_configure(wmSurf, serial) C.xdg_surface_ack_configure(wmSurf, serial)
w.setStage(system.StageRunning) w.configured = true
w.draw(true)
} }
//export gio_onToplevelClose //export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) { func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := callbackLoad(data).(*window) w := callbackLoad(data).(*window)
w.dead = true w.closing = true
} }
//export gio_onToplevelConfigure //export gio_onToplevelConfigure
@@ -586,8 +583,8 @@ func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_
} else { } else {
w.size.Y += int(w.config.decoHeight) w.size.Y += int(w.config.decoHeight)
} }
w.w.Event(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
w.redraw = true w.draw(true)
} }
} }
@@ -645,7 +642,7 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *
if w.config.Mode == Minimized { if w.config.Mode == Minimized {
// Minimized window got brought back up: it is no longer so. // Minimized window got brought back up: it is no longer so.
w.config.Mode = Windowed w.config.Mode = Windowed
w.w.Event(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
} }
} }
@@ -790,8 +787,8 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.
X: fromFixed(x) * float32(w.scale), X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale), Y: fromFixed(y) * float32(w.scale),
} }
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: pointer.Press, Kind: pointer.Press,
Source: pointer.Touch, Source: pointer.Touch,
Position: w.lastTouch, Position: w.lastTouch,
PointerID: pointer.ID(id), PointerID: pointer.ID(id),
@@ -806,8 +803,8 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui
s.serial = serial s.serial = serial
w := s.touchFoci[id] w := s.touchFoci[id]
delete(s.touchFoci, id) delete(s.touchFoci, id)
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: pointer.Release, Kind: pointer.Release,
Source: pointer.Touch, Source: pointer.Touch,
Position: w.lastTouch, Position: w.lastTouch,
PointerID: pointer.ID(id), PointerID: pointer.ID(id),
@@ -824,8 +821,8 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32
X: fromFixed(x) * float32(w.scale), X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale), Y: fromFixed(y) * float32(w.scale),
} }
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: pointer.Move, Kind: pointer.Move,
Position: w.lastTouch, Position: w.lastTouch,
Source: pointer.Touch, Source: pointer.Touch,
PointerID: pointer.ID(id), PointerID: pointer.ID(id),
@@ -843,8 +840,8 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
s := callbackLoad(data).(*wlSeat) s := callbackLoad(data).(*wlSeat)
for id, w := range s.touchFoci { for id, w := range s.touchFoci {
delete(s.touchFoci, id) delete(s.touchFoci, id)
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: pointer.Cancel, Kind: pointer.Cancel,
Source: pointer.Touch, Source: pointer.Touch,
}) })
} }
@@ -869,7 +866,7 @@ func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.ui
s.serial = serial s.serial = serial
if w.inCompositor { if w.inCompositor {
w.inCompositor = false w.inCompositor = false
w.w.Event(pointer.Event{Type: pointer.Cancel}) w.ProcessEvent(pointer.Event{Kind: pointer.Cancel})
} }
} }
@@ -917,21 +914,21 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
} }
} }
} }
var typ pointer.Type var kind pointer.Kind
switch state { switch state {
case 0: case 0:
w.pointerBtns &^= btn w.pointerBtns &^= btn
typ = pointer.Release kind = pointer.Release
// Move or resize gestures no longer applies. // Move or resize gestures no longer applies.
w.inCompositor = false w.inCompositor = false
case 1: case 1:
w.pointerBtns |= btn w.pointerBtns |= btn
typ = pointer.Press kind = pointer.Press
} }
w.flushScroll() w.flushScroll()
w.resetFling() w.resetFling()
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: typ, Kind: kind,
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: w.pointerBtns, Buttons: w.pointerBtns,
Position: w.lastPos, Position: w.lastPos,
@@ -1018,23 +1015,34 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis
} }
func (w *window) ReadClipboard() { func (w *window) ReadClipboard() {
if w.disp.readClipClose != nil {
return
}
w.disp.readClipClose = make(chan struct{})
r, err := w.disp.readClipboard() r, err := w.disp.readClipboard()
// Send empty responses on unavailable clipboards or errors.
if r == nil || err != nil { if r == nil || err != nil {
w.w.Event(clipboard.Event{})
return return
} }
// Don't let slow clipboard transfers block event loop. // Don't let slow clipboard transfers block event loop.
go func() { go func() {
defer r.Close() defer r.Close()
data, _ := io.ReadAll(r) data, _ := io.ReadAll(r)
w.clipReads <- clipboard.Event{Text: string(data)} e := transfer.DataEvent{
w.Wakeup() Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(data))
},
}
select {
case w.clipReads <- e:
w.disp.wakeup()
case <-w.disp.readClipClose:
}
}() }()
} }
func (w *window) WriteClipboard(s string) { func (w *window) WriteClipboard(mime string, s []byte) {
w.disp.writeClipboard([]byte(s)) w.disp.writeClipboard(s)
} }
func (w *window) Configure(options []Option) { func (w *window) Configure(options []Option) {
@@ -1092,8 +1100,7 @@ func (w *window) Configure(options []Option) {
w.config.MaxSize = cnf.MaxSize w.config.MaxSize = cnf.MaxSize
w.setWindowConstraints() w.setWindowConstraints()
} }
w.w.Event(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
w.redraw = true
} }
func (w *window) setWindowConstraints() { func (w *window) setWindowConstraints() {
@@ -1130,7 +1137,7 @@ func (w *window) Perform(actions system.Action) {
walkActions(actions, func(action system.Action) { walkActions(actions, func(action system.Action) {
switch action { switch action {
case system.ActionClose: case system.ActionClose:
w.dead = true w.closing = true
} }
}) })
} }
@@ -1213,7 +1220,8 @@ func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
w := callbackLoad(unsafe.Pointer(surf)).(*window) w := callbackLoad(unsafe.Pointer(surf)).(*window)
s.keyboardFocus = w s.keyboardFocus = w
s.disp.repeat.Stop(0) s.disp.repeat.Stop(0)
w.w.Event(key.FocusEvent{Focus: true}) w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export gio_onKeyboardLeave //export gio_onKeyboardLeave
@@ -1222,7 +1230,8 @@ func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
s.serial = serial s.serial = serial
s.disp.repeat.Stop(0) s.disp.repeat.Stop(0)
w := s.keyboardFocus w := s.keyboardFocus
w.w.Event(key.FocusEvent{Focus: false}) w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export gio_onKeyboardKey //export gio_onKeyboardKey
@@ -1240,7 +1249,7 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
// 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)
} }
} }
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED { if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
@@ -1275,7 +1284,7 @@ func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
r.now = 0 r.now = 0
r.stopC = stopC r.stopC = stopC
r.key = keyCode r.key = keyCode
r.win = w.w r.win = w
rate, delay := r.rate, r.delay rate, delay := r.rate, r.delay
go func() { go func() {
timer := time.NewTimer(delay) timer := time.NewTimer(delay)
@@ -1333,9 +1342,9 @@ func (r *repeatState) Repeat(d *wlDisplay) {
for _, e := range d.xkb.DispatchKey(r.key, key.Press) { for _, e := range d.xkb.DispatchKey(r.key, key.Press) {
if ee, ok := e.(key.EditEvent); ok { if ee, ok := e.(key.EditEvent); ok {
// There's no support for IME yet. // There's no support for IME yet.
r.win.EditorInsert(ee.Text) r.win.w.EditorInsert(ee.Text)
} else { } else {
r.win.Event(e) r.win.ProcessEvent(e)
} }
} }
r.last += delay r.last += delay
@@ -1348,28 +1357,68 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
w := callbackLoad(data).(*window) w := callbackLoad(data).(*window)
if w.lastFrameCallback == callback { if w.lastFrameCallback == callback {
w.lastFrameCallback = nil w.lastFrameCallback = nil
w.draw(false)
} }
} }
func (w *window) loop() error { func (w *window) close(err error) {
var p poller w.ProcessEvent(WaylandViewEvent{})
for { w.ProcessEvent(DestroyEvent{Err: err})
if err := w.disp.dispatch(&p); err != nil { w.destroy()
return err w.disp.destroy()
} w.disp = nil
select { }
case e := <-w.clipReads:
w.w.Event(e) func (w *window) dispatch() {
case <-w.wakeups: if w.disp == nil {
w.w.Event(wakeupEvent{}) <-w.wakeups
default: w.w.Invalidate()
} return
if w.dead {
break
}
w.draw()
} }
return nil if err := w.disp.dispatch(); err != nil || w.closing {
w.close(err)
return
}
select {
case e := <-w.clipReads:
w.disp.readClipClose = nil
w.ProcessEvent(e)
case <-w.wakeups:
w.w.Invalidate()
default:
}
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
}
func (w *window) Event() event.Event {
for {
evt, ok := w.w.nextEvent()
if !ok {
w.dispatch()
continue
}
return evt
}
}
func (w *window) Invalidate() {
select {
case w.wakeups <- struct{}{}:
default:
return
}
w.disp.wakeup()
}
func (w *window) Run(f func()) {
f()
}
func (w *window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
} }
// bindDataDevice initializes the dataDev field if and only if both // bindDataDevice initializes the dataDev field if and only if both
@@ -1385,13 +1434,21 @@ func (d *wlDisplay) bindDataDevice() {
} }
} }
func (d *wlDisplay) dispatch(p *poller) error { func (d *wlDisplay) dispatch() error {
// wl_display_prepare_read records the current thread for
// use in wl_display_read_events or wl_display_cancel_events.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
dispfd := C.wl_display_get_fd(d.disp) dispfd := C.wl_display_get_fd(d.disp)
// Poll for events and notifications. // Poll for events and notifications.
pollfds := append(p.pollfds[:0], pollfds := append(d.poller.pollfds[:0],
syscall.PollFd{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR}, syscall.PollFd{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR},
syscall.PollFd{Fd: int32(d.notify.read), Events: syscall.POLLIN | syscall.POLLERR}, syscall.PollFd{Fd: int32(d.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
) )
for C.wl_display_prepare_read(d.disp) != 0 {
C.wl_display_dispatch_pending(d.disp)
}
dispFd := &pollfds[0] dispFd := &pollfds[0]
if ret, err := C.wl_display_flush(d.disp); ret < 0 { if ret, err := C.wl_display_flush(d.disp); ret < 0 {
if err != syscall.EAGAIN { if err != syscall.EAGAIN {
@@ -1402,11 +1459,25 @@ func (d *wlDisplay) dispatch(p *poller) error {
dispFd.Events |= syscall.POLLOUT dispFd.Events |= syscall.POLLOUT
} }
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR { if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
C.wl_display_cancel_read(d.disp)
return fmt.Errorf("wayland: poll failed: %v", err) return fmt.Errorf("wayland: poll failed: %v", err)
} }
if dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0 {
C.wl_display_cancel_read(d.disp)
return errors.New("wayland: display file descriptor gone")
}
// Handle events.
if dispFd.Revents&syscall.POLLIN != 0 {
if ret, err := C.wl_display_read_events(d.disp); ret < 0 {
return fmt.Errorf("wayland: wl_display_read_events failed: %v", err)
}
C.wl_display_dispatch_pending(d.disp)
} else {
C.wl_display_cancel_read(d.disp)
}
// Clear notifications. // Clear notifications.
for { for {
_, err := syscall.Read(d.notify.read, p.buf[:]) _, err := syscall.Read(d.notify.read, d.poller.buf[:])
if err == syscall.EAGAIN { if err == syscall.EAGAIN {
break break
} }
@@ -1414,29 +1485,15 @@ func (d *wlDisplay) dispatch(p *poller) error {
return fmt.Errorf("wayland: read from notify pipe failed: %v", err) return fmt.Errorf("wayland: read from notify pipe failed: %v", err)
} }
} }
// Handle events
switch {
case dispFd.Revents&syscall.POLLIN != 0:
if ret, err := C.wl_display_dispatch(d.disp); ret < 0 {
return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err)
}
case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0:
return errors.New("wayland: display file descriptor gone")
}
d.repeat.Repeat(d) d.repeat.Repeat(d)
return nil return nil
} }
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
w.disp.wakeup()
}
func (w *window) SetAnimating(anim bool) { func (w *window) SetAnimating(anim bool) {
w.animating = anim w.animating = anim
if anim {
w.draw(false)
}
} }
// Wakeup wakes up the event loop through the notification pipe. // Wakeup wakes up the event loop through the notification pipe.
@@ -1448,6 +1505,10 @@ func (d *wlDisplay) wakeup() {
} }
func (w *window) destroy() { func (w *window) destroy() {
if w.lastFrameCallback != nil {
C.wl_callback_destroy(w.lastFrameCallback)
w.lastFrameCallback = nil
}
if w.cursor.surf != nil { if w.cursor.surf != nil {
C.wl_surface_destroy(w.cursor.surf) C.wl_surface_destroy(w.cursor.surf)
} }
@@ -1572,8 +1633,16 @@ func (w *window) flushScroll() {
if total == (f32.Point{}) { if total == (f32.Point{}) {
return return
} }
w.w.Event(pointer.Event{ if w.scroll.steps == (image.Point{}) {
Type: pointer.Scroll, w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
}
// Zero scroll distance prior to calling ProcessEvent, otherwise we may recursively
// re-process the scroll distance.
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
w.ProcessEvent(pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: w.pointerBtns, Buttons: w.pointerBtns,
Position: w.lastPos, Position: w.lastPos,
@@ -1581,12 +1650,6 @@ func (w *window) flushScroll() {
Time: w.scroll.time, Time: w.scroll.time,
Modifiers: w.disp.xkb.Modifiers(), Modifiers: w.disp.xkb.Modifiers(),
}) })
if w.scroll.steps == (image.Point{}) {
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
}
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
} }
func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) { func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
@@ -1595,8 +1658,8 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
X: fromFixed(x) * float32(w.scale), X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale), Y: fromFixed(y) * float32(w.scale),
} }
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: pointer.Move, Kind: pointer.Move,
Position: w.lastPos, Position: w.lastPos,
Buttons: w.pointerBtns, Buttons: w.pointerBtns,
Source: pointer.Mouse, Source: pointer.Mouse,
@@ -1616,7 +1679,8 @@ func (w *window) systemGesture() (*C.struct_wl_cursor, C.uint32_t) {
if w.config.Mode != Windowed || w.config.Decorated { if w.config.Mode != Windowed || w.config.Decorated {
return nil, 0 return nil, 0
} }
border := w.w.w.metric.Dp(3) _, cfg := w.getConfig()
border := cfg.Dp(3)
x, y, size := int(w.lastPos.X), int(w.lastPos.Y), w.config.Size x, y, size := int(w.lastPos.X), int(w.lastPos.Y), w.config.Size
north := y <= border north := y <= border
south := y >= size.Y-border south := y >= size.Y-border
@@ -1670,13 +1734,10 @@ func (w *window) updateOutputs() {
if found && scale != w.scale { if found && scale != w.scale {
w.scale = scale w.scale = scale
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale)) C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
w.redraw = true w.draw(true)
} }
if !found { if found {
w.setStage(system.StagePaused) w.draw(true)
} else {
w.setStage(system.StageRunning)
w.redraw = true
} }
} }
@@ -1688,7 +1749,10 @@ func (w *window) getConfig() (image.Point, unit.Metric) {
} }
} }
func (w *window) draw() { func (w *window) draw(sync bool) {
if !w.configured {
return
}
w.flushScroll() w.flushScroll()
size, cfg := w.getConfig() size, cfg := w.getConfig()
if cfg == (unit.Metric{}) { if cfg == (unit.Metric{}) {
@@ -1696,11 +1760,9 @@ func (w *window) draw() {
} }
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})
} }
anim := w.animating || w.fling.anim.Active() anim := w.animating || w.fling.anim.Active()
sync := w.redraw
w.redraw = false
// Draw animation only when not waiting for frame callback. // Draw animation only when not waiting for frame callback.
redrawAnim := anim && w.lastFrameCallback == nil redrawAnim := anim && w.lastFrameCallback == nil
if !redrawAnim && !sync { if !redrawAnim && !sync {
@@ -1711,8 +1773,8 @@ func (w *window) draw() {
// Use the surface as listener data for gio_onFrameDone. // Use the surface as listener data for gio_onFrameDone.
C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf)) C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf))
} }
w.w.Event(frameEvent{ w.ProcessEvent(frameEvent{
FrameEvent: system.FrameEvent{ FrameEvent: FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Metric: cfg, Metric: cfg,
@@ -1721,14 +1783,6 @@ func (w *window) draw() {
}) })
} }
func (w *window) setStage(s system.Stage) {
if s == w.stage {
return
}
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
}
func (w *window) display() *C.struct_wl_display { func (w *window) display() *C.struct_wl_display {
return w.disp.disp return w.disp.disp
} }
@@ -1820,6 +1874,10 @@ func newWLDisplay() (*wlDisplay, error) {
} }
func (d *wlDisplay) destroy() { func (d *wlDisplay) destroy() {
if d.readClipClose != nil {
close(d.readClipClose)
d.readClipClose = nil
}
if d.notify.write != 0 { if d.notify.write != 0 {
syscall.Close(d.notify.write) syscall.Close(d.notify.write)
d.notify.write = 0 d.notify.write = 0
@@ -1861,6 +1919,7 @@ func (d *wlDisplay) destroy() {
if d.disp != nil { if d.disp != nil {
C.wl_display_disconnect(d.disp) C.wl_display_disconnect(d.disp)
callbackDelete(unsafe.Pointer(d.disp)) callbackDelete(unsafe.Pointer(d.disp))
d.disp = nil
} }
} }
+163 -128
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"io"
"runtime" "runtime"
"sort" "sort"
"strings" "strings"
@@ -18,30 +19,26 @@ import (
syscall "golang.org/x/sys/windows" syscall "golang.org/x/sys/windows"
"gioui.org/app/internal/windows" "gioui.org/app/internal/windows"
"gioui.org/op"
"gioui.org/unit" "gioui.org/unit"
gowindows "golang.org/x/sys/windows" gowindows "golang.org/x/sys/windows"
"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"
) )
type ViewEvent struct { type Win32ViewEvent struct {
HWND uintptr HWND uintptr
} }
type winDeltas struct {
width int32
height int32
}
type window struct { type window struct {
hwnd syscall.Handle hwnd syscall.Handle
hdc syscall.Handle hdc syscall.Handle
w *callbacks w *callbacks
stage system.Stage
pointerBtns pointer.Buttons pointerBtns pointer.Buttons
// cursorIn tracks whether the cursor was inside the window according // cursorIn tracks whether the cursor was inside the window according
@@ -53,11 +50,10 @@ type window struct {
placement *windows.WindowPlacement placement *windows.WindowPlacement
animating bool animating bool
focused bool
deltas winDeltas
borderSize image.Point borderSize image.Point
config Config config Config
loop *eventLoop
} }
const _WM_WAKEUP = windows.WM_USER + iota const _WM_WAKEUP = windows.WM_USER + iota
@@ -90,36 +86,38 @@ func osMain() {
select {} select {}
} }
func newWindow(window *callbacks, options []Option) error { func newWindow(win *callbacks, options []Option) {
cerr := make(chan error) done := make(chan struct{})
go func() { go func() {
// GetMessage and PeekMessage can filter on a window HWND, but // GetMessage and PeekMessage can filter on a window HWND, but
// then thread-specific messages such as WM_QUIT are ignored. // then thread-specific messages such as WM_QUIT are ignored.
// Instead lock the thread so window messages arrive through // Instead lock the thread so window messages arrive through
// unfiltered GetMessage calls. // unfiltered GetMessage calls.
runtime.LockOSThread() runtime.LockOSThread()
w, err := createNativeWindow()
w := &window{
w: win,
}
w.loop = newEventLoop(w.w, w.wakeup)
w.w.SetDriver(w)
err := w.init()
done <- struct{}{}
if err != nil { if err != nil {
cerr <- err w.ProcessEvent(DestroyEvent{Err: err})
return return
} }
cerr <- nil
winMap.Store(w.hwnd, w) winMap.Store(w.hwnd, w)
defer winMap.Delete(w.hwnd) defer winMap.Delete(w.hwnd)
w.w = window w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)})
w.w.SetDriver(w)
w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)})
w.Configure(options) w.Configure(options)
windows.SetForegroundWindow(w.hwnd) windows.SetForegroundWindow(w.hwnd)
windows.SetFocus(w.hwnd) windows.SetFocus(w.hwnd)
// Since the window class for the cursor is null, // Since the window class for the cursor is null,
// set it here to show the cursor. // set it here to show the cursor.
w.SetCursor(pointer.CursorDefault) w.SetCursor(pointer.CursorDefault)
if err := w.loop(); err != nil { w.runLoop()
panic(err)
}
}() }()
return <-cerr <-done
} }
// initResources initializes the resources global. // initResources initializes the resources global.
@@ -154,13 +152,13 @@ func initResources() error {
const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE
func createNativeWindow() (*window, error) { func (w *window) init() error {
var resErr error var resErr error
resources.once.Do(func() { resources.once.Do(func() {
resErr = initResources() resErr = initResources()
}) })
if resErr != nil { if resErr != nil {
return nil, resErr return resErr
} }
const dwStyle = windows.WS_OVERLAPPEDWINDOW const dwStyle = windows.WS_OVERLAPPEDWINDOW
@@ -176,43 +174,32 @@ func createNativeWindow() (*window, error) {
resources.handle, resources.handle,
0) 0)
if err != nil { if err != nil {
return nil, err return err
}
w := &window{
hwnd: hwnd,
} }
w.hdc, err = windows.GetDC(hwnd) w.hdc, err = windows.GetDC(hwnd)
if err != nil { if err != nil {
return nil, err windows.DestroyWindow(hwnd)
return err
} }
return w, nil w.hwnd = hwnd
return nil
} }
// update() handles changes done by the user, and updates the configuration. // update() handles changes done by the user, and updates the configuration.
// It reads the window style and size/position and updates w.config. // It reads the window style and size/position and updates w.config.
// If anything has changed it emits a ConfigEvent to notify the application. // If anything has changed it emits a ConfigEvent to notify the application.
func (w *window) update() { func (w *window) update() {
r := windows.GetWindowRect(w.hwnd) cr := windows.GetClientRect(w.hwnd)
size := image.Point{ w.config.Size = image.Point{
X: int(r.Right - r.Left - w.deltas.width), X: int(cr.Right - cr.Left),
Y: int(r.Bottom - r.Top - w.deltas.height), Y: int(cr.Bottom - cr.Top),
} }
// Check the window mode.
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
if style&windows.WS_OVERLAPPEDWINDOW == 0 {
size = image.Point{
X: int(r.Right - r.Left),
Y: int(r.Bottom - r.Top),
}
}
w.config.Size = size
w.borderSize = image.Pt( w.borderSize = image.Pt(
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME), windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME), windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
) )
w.w.Event(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
} }
func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr { func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
@@ -253,7 +240,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
e.State = key.Release e.State = key.Release
} }
w.w.Event(e) w.ProcessEvent(e)
if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) { if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) {
// Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs // Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs
@@ -274,23 +261,15 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MBUTTONUP: case windows.WM_MBUTTONUP:
w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers()) w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers())
case windows.WM_CANCELMODE: case windows.WM_CANCELMODE:
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: pointer.Cancel, Kind: pointer.Cancel,
}) })
case windows.WM_SETFOCUS: case windows.WM_SETFOCUS:
w.focused = true w.config.Focused = true
w.w.Event(key.FocusEvent{Focus: true}) w.ProcessEvent(ConfigEvent{Config: w.config})
case windows.WM_KILLFOCUS: case windows.WM_KILLFOCUS:
w.focused = false w.config.Focused = false
w.w.Event(key.FocusEvent{Focus: false}) w.ProcessEvent(ConfigEvent{Config: w.config})
case windows.WM_NCACTIVATE:
if w.stage >= system.StageInactive {
if wParam == windows.TRUE {
w.setStage(system.StageRunning)
} else {
w.setStage(system.StageInactive)
}
}
case windows.WM_NCHITTEST: case windows.WM_NCHITTEST:
if w.config.Decorated { if w.config.Decorated {
// Let the system handle it. // Let the system handle it.
@@ -303,8 +282,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MOUSEMOVE: case windows.WM_MOUSEMOVE:
x, y := coordsFromlParam(lParam) x, y := coordsFromlParam(lParam)
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: pointer.Move, Kind: pointer.Move,
Source: pointer.Mouse, Source: pointer.Mouse,
Position: p, Position: p,
Buttons: w.pointerBtns, Buttons: w.pointerBtns,
@@ -316,8 +295,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MOUSEHWHEEL: case windows.WM_MOUSEHWHEEL:
w.scrollEvent(wParam, lParam, true, getModifiers()) w.scrollEvent(wParam, lParam, true, getModifiers())
case windows.WM_DESTROY: case windows.WM_DESTROY:
w.w.Event(ViewEvent{}) w.ProcessEvent(Win32ViewEvent{})
w.w.Event(system.DestroyEvent{}) w.ProcessEvent(DestroyEvent{})
if w.hdc != 0 { if w.hdc != 0 {
windows.ReleaseDC(w.hdc) windows.ReleaseDC(w.hdc)
w.hdc = 0 w.hdc = 0
@@ -325,6 +304,34 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
// The system destroys the HWND for us. // The system destroys the HWND for us.
w.hwnd = 0 w.hwnd = 0
windows.PostQuitMessage(0) windows.PostQuitMessage(0)
case windows.WM_NCCALCSIZE:
if w.config.Decorated {
// Let Windows handle decorations.
break
}
// No client areas; we draw decorations ourselves.
if wParam != 1 {
return 0
}
// lParam contains an NCCALCSIZE_PARAMS for us to adjust.
place := windows.GetWindowPlacement(w.hwnd)
if !place.IsMaximized() {
// Nothing do adjust.
return 0
}
// Adjust window position to avoid the extra padding in maximized
// state. See https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543.
// Note that trying to do the adjustment in WM_GETMINMAXINFO is ignored by Windows.
szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(lParam))
mi := windows.GetMonitorInfo(w.hwnd)
szp.Rgrc[0] = mi.WorkArea
return 0
case windows.WM_NCLBUTTONDBLCLK:
if !w.config.Decorated {
// Override Windows behaviour when we
// draw decorations.
return 0
}
case windows.WM_PAINT: case windows.WM_PAINT:
w.draw(true) w.draw(true)
case windows.WM_SIZE: case windows.WM_SIZE:
@@ -332,30 +339,35 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
switch wParam { switch wParam {
case windows.SIZE_MINIMIZED: case windows.SIZE_MINIMIZED:
w.config.Mode = Minimized w.config.Mode = Minimized
w.setStage(system.StagePaused)
case windows.SIZE_MAXIMIZED: case windows.SIZE_MAXIMIZED:
w.config.Mode = Maximized w.config.Mode = Maximized
w.setStage(system.StageRunning)
case windows.SIZE_RESTORED: case windows.SIZE_RESTORED:
if w.config.Mode != Fullscreen { if w.config.Mode != Fullscreen {
w.config.Mode = Windowed w.config.Mode = Windowed
} }
w.setStage(system.StageRunning)
} }
case windows.WM_GETMINMAXINFO: case windows.WM_GETMINMAXINFO:
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam))) mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam))
var bw, bh int32
if w.config.Decorated {
r := windows.GetWindowRect(w.hwnd)
cr := windows.GetClientRect(w.hwnd)
bw = r.Right - r.Left - (cr.Right - cr.Left)
bh = r.Bottom - r.Top - (cr.Bottom - cr.Top)
}
if p := w.config.MinSize; p.X > 0 || p.Y > 0 { if p := w.config.MinSize; p.X > 0 || p.Y > 0 {
mm.PtMinTrackSize = windows.Point{ mm.PtMinTrackSize = windows.Point{
X: int32(p.X) + w.deltas.width, X: int32(p.X) + bw,
Y: int32(p.Y) + w.deltas.height, Y: int32(p.Y) + bh,
} }
} }
if p := w.config.MaxSize; p.X > 0 || p.Y > 0 { if p := w.config.MaxSize; p.X > 0 || p.Y > 0 {
mm.PtMaxTrackSize = windows.Point{ mm.PtMaxTrackSize = windows.Point{
X: int32(p.X) + w.deltas.width, X: int32(p.X) + bw,
Y: int32(p.Y) + w.deltas.height, Y: int32(p.Y) + bh,
} }
} }
return 0
case windows.WM_SETCURSOR: case windows.WM_SETCURSOR:
w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT
if w.cursorIn { if w.cursorIn {
@@ -363,7 +375,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
return windows.TRUE return windows.TRUE
} }
case _WM_WAKEUP: case _WM_WAKEUP:
w.w.Event(wakeupEvent{}) w.loop.Wakeup()
w.loop.FlushEvents()
case windows.WM_IME_STARTCOMPOSITION: case windows.WM_IME_STARTCOMPOSITION:
imc := windows.ImmGetContext(w.hwnd) imc := windows.ImmGetContext(w.hwnd)
if imc == 0 { if imc == 0 {
@@ -446,23 +459,18 @@ func (w *window) hitTest(x, y int) uintptr {
if w.config.Mode == Fullscreen { if w.config.Mode == Fullscreen {
return windows.HTCLIENT return windows.HTCLIENT
} }
p := f32.Pt(float32(x), float32(y))
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
return windows.HTCAPTION
}
if w.config.Mode != Windowed { if w.config.Mode != Windowed {
// Only windowed mode should allow resizing. // Only windowed mode should allow resizing.
return windows.HTCLIENT return windows.HTCLIENT
} }
// Check for resize handle before system actions; otherwise it can be impossible to
// resize a custom-decorations window when the system move area is flush with the
// edge of the window.
top := y <= w.borderSize.Y top := y <= w.borderSize.Y
bottom := y >= w.config.Size.Y-w.borderSize.Y bottom := y >= w.config.Size.Y-w.borderSize.Y
left := x <= w.borderSize.X left := x <= w.borderSize.X
right := x >= w.config.Size.X-w.borderSize.X right := x >= w.config.Size.X-w.borderSize.X
switch { switch {
default:
fallthrough
case !top && !bottom && !left && !right:
return windows.HTCLIENT
case top && left: case top && left:
return windows.HTTOPLEFT return windows.HTTOPLEFT
case top && right: case top && right:
@@ -480,22 +488,27 @@ func (w *window) hitTest(x, y int) uintptr {
case right: case right:
return windows.HTRIGHT return windows.HTRIGHT
} }
p := f32.Pt(float32(x), float32(y))
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
return windows.HTCAPTION
}
return windows.HTCLIENT
} }
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) { func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
if !w.focused { if !w.config.Focused {
windows.SetFocus(w.hwnd) windows.SetFocus(w.hwnd)
} }
var typ pointer.Type var kind pointer.Kind
if press { if press {
typ = pointer.Press kind = pointer.Press
if w.pointerBtns == 0 { if w.pointerBtns == 0 {
windows.SetCapture(w.hwnd) windows.SetCapture(w.hwnd)
} }
w.pointerBtns |= btn w.pointerBtns |= btn
} else { } else {
typ = pointer.Release kind = pointer.Release
w.pointerBtns &^= btn w.pointerBtns &^= btn
if w.pointerBtns == 0 { if w.pointerBtns == 0 {
windows.ReleaseCapture() windows.ReleaseCapture()
@@ -503,8 +516,8 @@ func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr,
} }
x, y := coordsFromlParam(lParam) x, y := coordsFromlParam(lParam)
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.Mouse, Source: pointer.Mouse,
Position: p, Position: p,
Buttons: w.pointerBtns, Buttons: w.pointerBtns,
@@ -538,8 +551,8 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
sp.Y = -dist sp.Y = -dist
} }
} }
w.w.Event(pointer.Event{ w.ProcessEvent(pointer.Event{
Type: pointer.Scroll, Kind: pointer.Scroll,
Source: pointer.Mouse, Source: pointer.Mouse,
Position: p, Position: p,
Buttons: w.pointerBtns, Buttons: w.pointerBtns,
@@ -550,7 +563,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
} }
// Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/ // Adapted from https://blogs.msdn.microsoft.com/oldnewthing/20060126-00/?p=32513/
func (w *window) loop() error { func (w *window) runLoop() {
msg := new(windows.Msg) msg := new(windows.Msg)
loop: loop:
for { for {
@@ -561,7 +574,7 @@ loop:
} }
switch ret := windows.GetMessage(msg, 0, 0, 0); ret { switch ret := windows.GetMessage(msg, 0, 0, 0); ret {
case -1: case -1:
return errors.New("GetMessage failed") panic(errors.New("GetMessage failed"))
case 0: case 0:
// WM_QUIT received. // WM_QUIT received.
break loop break loop
@@ -569,7 +582,6 @@ loop:
windows.TranslateMessage(msg) windows.TranslateMessage(msg)
windows.DispatchMessage(msg) windows.DispatchMessage(msg)
} }
return nil
} }
func (w *window) EditorStateChanged(old, new editorState) { func (w *window) EditorStateChanged(old, new editorState) {
@@ -587,16 +599,30 @@ func (w *window) SetAnimating(anim bool) {
w.animating = anim w.animating = anim
} }
func (w *window) Wakeup() { func (w *window) ProcessEvent(e event.Event) {
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil { w.w.ProcessEvent(e)
panic(err) w.loop.FlushEvents()
}
} }
func (w *window) setStage(s system.Stage) { func (w *window) Event() event.Event {
if s != w.stage { return w.loop.Event()
w.stage = s }
w.w.Event(system.StageEvent{Stage: s})
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func (w *window) wakeup() {
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
panic(err)
} }
} }
@@ -606,8 +632,8 @@ func (w *window) draw(sync bool) {
} }
dpi := windows.GetWindowDPI(w.hwnd) dpi := windows.GetWindowDPI(w.hwnd)
cfg := configForDPI(dpi) cfg := configForDPI(dpi)
w.w.Event(frameEvent{ w.ProcessEvent(frameEvent{
FrameEvent: system.FrameEvent{ FrameEvent: FrameEvent{
Now: time.Now(), Now: time.Now(),
Size: w.config.Size, Size: w.config.Size,
Metric: cfg, Metric: cfg,
@@ -653,7 +679,12 @@ func (w *window) readClipboard() error {
} }
defer windows.GlobalUnlock(mem) defer windows.GlobalUnlock(mem)
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
w.w.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
} }
@@ -669,9 +700,6 @@ func (w *window) Configure(options []Option) {
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED) swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW) winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
style &^= winStyle style &^= winStyle
if !w.config.Decorated {
winStyle = 0
}
switch w.config.Mode { switch w.config.Mode {
case Minimized: case Minimized:
style |= winStyle style |= winStyle
@@ -684,45 +712,48 @@ func (w *window) Configure(options []Option) {
showMode = windows.SW_SHOWMAXIMIZED showMode = windows.SW_SHOWMAXIMIZED
case Windowed: case Windowed:
windows.SetWindowText(w.hwnd, w.config.Title)
style |= winStyle style |= winStyle
showMode = windows.SW_SHOWNORMAL showMode = windows.SW_SHOWNORMAL
// Get target for client areaa size. // Get target for client area size.
width = int32(w.config.Size.X) width = int32(w.config.Size.X)
height = int32(w.config.Size.Y) height = int32(w.config.Size.Y)
// Get the current window size and position. // Get the current window size and position.
wr := windows.GetWindowRect(w.hwnd) wr := windows.GetWindowRect(w.hwnd)
// Set desired window size.
wr.Right = wr.Left + width
wr.Bottom = wr.Top + height
// Convert from client size to window size.
r := wr
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
// Calculate difference between client and full window sizes.
w.deltas.width = r.Right - wr.Right + wr.Left - r.Left
w.deltas.height = r.Bottom - wr.Bottom + wr.Top - r.Top
// Set new window size and position.
x = wr.Left x = wr.Left
y = wr.Top y = wr.Top
width = r.Right - r.Left if w.config.Decorated {
height = r.Bottom - r.Top // Compute client size and position. Note that the client size is
// equal to the window size when we are in control of decorations.
r := windows.Rect{
Right: width,
Bottom: height,
}
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
width = r.Right - r.Left
height = r.Bottom - r.Top
}
if !w.config.Decorated {
// Enable drop shadows when we draw decorations.
windows.DwmExtendFrameIntoClientArea(w.hwnd, windows.Margins{-1, -1, -1, -1})
}
case Fullscreen: case Fullscreen:
swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE
mi := windows.GetMonitorInfo(w.hwnd) mi := windows.GetMonitorInfo(w.hwnd)
x, y = mi.Monitor.Left, mi.Monitor.Top x, y = mi.Monitor.Left, mi.Monitor.Top
width = mi.Monitor.Right - mi.Monitor.Left width = mi.Monitor.Right - mi.Monitor.Left
height = mi.Monitor.Bottom - mi.Monitor.Top height = mi.Monitor.Bottom - mi.Monitor.Top
showMode = windows.SW_SHOW showMode = windows.SW_SHOWMAXIMIZED
} }
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style) windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style)
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle) windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle)
windows.ShowWindow(w.hwnd, showMode) windows.ShowWindow(w.hwnd, showMode)
w.w.Event(ConfigEvent{Config: w.config}) w.update()
} }
func (w *window) WriteClipboard(s string) { func (w *window) WriteClipboard(mime string, s []byte) {
w.writeClipboard(s) w.writeClipboard(string(s))
} }
func (w *window) writeClipboard(s string) error { func (w *window) writeClipboard(s string) error {
@@ -850,11 +881,11 @@ func (w *window) raise() {
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW) windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
} }
func convertKeyCode(code uintptr) (string, bool) { func convertKeyCode(code uintptr) (key.Name, bool) {
if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' { if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' {
return string(rune(code)), true return key.Name(rune(code)), true
} }
var r string var r key.Name
switch code { switch code {
case windows.VK_ESCAPE: case windows.VK_ESCAPE:
@@ -954,4 +985,8 @@ func configForDPI(dpi int) unit.Metric {
} }
} }
func (_ ViewEvent) ImplementsEvent() {} func (Win32ViewEvent) implementsViewEvent() {}
func (Win32ViewEvent) ImplementsEvent() {}
func (w Win32ViewEvent) Valid() bool {
return w != (Win32ViewEvent{})
}
+122 -90
View File
@@ -30,16 +30,20 @@ import (
"errors" "errors"
"fmt" "fmt"
"image" "image"
"io"
"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"
@@ -91,12 +95,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
@@ -109,6 +111,8 @@ type x11Window struct {
config Config config Config
wakeups chan struct{} wakeups chan struct{}
handler x11EventHandler
buf [100]byte
} }
var ( var (
@@ -151,8 +155,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)
} }
@@ -232,7 +236,7 @@ func (w *x11Window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated { if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated w.config.Decorated = cnf.Decorated
} }
w.w.Event(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
} }
func (w *x11Window) setTitle(prev, cnf Config) { func (w *x11Window) setTitle(prev, cnf Config) {
@@ -375,7 +379,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:
@@ -393,16 +426,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.
@@ -412,61 +449,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 { // Clear notifications.
_, err := syscall.Read(w.notify.read, buf) for {
if err == syscall.EAGAIN { _, err := syscall.Read(w.notify.read, w.buf[:])
break if err == syscall.EAGAIN {
} break
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
}
} }
select { if err != nil {
case <-w.wakeups: panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
w.w.Event(wakeupEvent{})
default:
}
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: w.metric,
},
Sync: syn,
})
} }
} }
if (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() {
@@ -484,6 +515,7 @@ 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
@@ -541,13 +573,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),
@@ -557,7 +589,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
@@ -569,7 +601,7 @@ func (h *x11EventHandler) handleEvents() bool {
case C.Button3: case C.Button3:
btn = pointer.ButtonSecondary btn = pointer.ButtonSecondary
case C.Button4: case C.Button4:
ev.Type = pointer.Scroll ev.Kind = pointer.Scroll
// scroll up or left (if shift is pressed). // scroll up or left (if shift is pressed).
if ev.Modifiers == key.ModShift { if ev.Modifiers == key.ModShift {
ev.Scroll.X = -scrollScale ev.Scroll.X = -scrollScale
@@ -578,7 +610,7 @@ func (h *x11EventHandler) handleEvents() bool {
} }
case C.Button5: case C.Button5:
// scroll down or right (if shift is pressed). // scroll down or right (if shift is pressed).
ev.Type = pointer.Scroll ev.Kind = pointer.Scroll
if ev.Modifiers == key.ModShift { if ev.Modifiers == key.ModShift {
ev.Scroll.X = +scrollScale ev.Scroll.X = +scrollScale
} else { } else {
@@ -587,11 +619,11 @@ func (h *x11EventHandler) handleEvents() bool {
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
@@ -603,11 +635,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{
@@ -621,14 +653,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:
@@ -650,7 +684,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 {
@@ -704,7 +743,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
} }
} }
@@ -786,8 +825,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()
@@ -823,19 +864,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.w.Event(system.DestroyEvent{Err: nil})
w.destroy()
}()
return nil return nil
} }
+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() {}
+403 -595
View File
File diff suppressed because it is too large Load Diff
Generated
+48 -17
View File
@@ -9,11 +9,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1659298920, "lastModified": 1701721028,
"narHash": "sha256-LgRMge8BZUG15EN43iDJOlnEMX1dvRprB7SaoNqgibU=", "narHash": "sha256-2z4YrdHPLoMZNWR1MPOjNZMqPg057i1eZXaYI6RTahQ=",
"owner": "tadfisher", "owner": "tadfisher",
"repo": "android-nixpkgs", "repo": "android-nixpkgs",
"rev": "d4f20a3cd4ce961bb23b48447457f6810d69ae5e", "rev": "c923f9ec0f4dd0d7dc725dc5b73fbf03658e50dd",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -24,21 +24,18 @@
}, },
"devshell": { "devshell": {
"inputs": { "inputs": {
"flake-utils": [
"android",
"nixpkgs"
],
"nixpkgs": [ "nixpkgs": [
"android", "android",
"nixpkgs" "nixpkgs"
] ],
"systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1658746384, "lastModified": 1701697687,
"narHash": "sha256-CCJcoMOcXyZFrV1ag4XMTpAPjLWb4Anbv+ktXFI1ry0=", "narHash": "sha256-dLLE5wQBVv+pIb4bWmKFSw2DvLVyuEk0F7ng6hpZPSU=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "0ffc7937bb5e8141af03d462b468bd071eb18e1b", "rev": "c3bd77911391eb1638af6ce773de86da57ee6df5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -48,12 +45,15 @@
} }
}, },
"flake-utils": { "flake-utils": {
"inputs": {
"systems": "systems_2"
},
"locked": { "locked": {
"lastModified": 1656928814, "lastModified": 1701680307,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -64,15 +64,16 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1659305579, "lastModified": 1701282334,
"narHash": "sha256-SFeQTmh7hc9Y2fSkooHaoS8mDfPa04sfmUCtQ8MA6Pg=", "narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5857574d45925585baffde730369414319228a84", "rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "23.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -82,6 +83,36 @@
"android": "android", "android": "android",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",
+4 -4
View File
@@ -3,7 +3,7 @@
description = "Gio build environment"; description = "Gio build environment";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs"; nixpkgs.url = "github:NixOS/nixpkgs/23.11";
android.url = "github:tadfisher/android-nixpkgs"; android.url = "github:tadfisher/android-nixpkgs";
android.inputs.nixpkgs.follows = "nixpkgs"; android.inputs.nixpkgs.follows = "nixpkgs";
}; };
@@ -33,10 +33,10 @@
default = with pkgs; mkShell default = with pkgs; mkShell
({ ({
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk"; ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
JAVA_HOME = jdk8.home; JAVA_HOME = jdk17.home;
packages = [ packages = [
android-sdk android-sdk
jdk8 jdk17
clang clang
] ++ (if stdenv.isLinux then [ ] ++ (if stdenv.isLinux then [
vulkan-headers vulkan-headers
@@ -46,7 +46,7 @@
xorg.libXcursor xorg.libXcursor
xorg.libXfixes xorg.libXfixes
libGL libGL
pkgconfig pkg-config
] else if stdenv.isDarwin then [ ] else if stdenv.isDarwin then [
darwin.apple_sdk_11_0.frameworks.Foundation darwin.apple_sdk_11_0.frameworks.Foundation
darwin.apple_sdk_11_0.frameworks.Metal darwin.apple_sdk_11_0.frameworks.Metal
+96 -86
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,20 +38,24 @@ 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
@@ -87,10 +92,10 @@ 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 image.Point Position image.Point
Source pointer.Source Source pointer.Source
Modifiers key.Modifiers Modifiers key.Modifiers
@@ -99,7 +104,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 +112,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 +119,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 +138,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 (
@@ -161,10 +163,7 @@ 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,24 +176,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 {
events = append(events, ClickEvent{Type: TypeClick, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}) return ClickEvent{
Kind: KindClick,
Position: e.Position.Round(),
Source: e.Source,
Modifiers: e.Modifiers,
NumClicks: c.clicks,
}, true
} else { } else {
events = append(events, ClickEvent{Type: TypeCancel}) return ClickEvent{Kind: KindCancel}, true
} }
case pointer.Cancel: case pointer.Cancel:
wasPressed := c.pressed wasPressed := c.pressed
@@ -202,7 +213,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 {
@@ -224,7 +235,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
c.clicks = 1 c.clicks = 1
} }
c.clickedAt = e.Time c.clickedAt = e.Time
events = append(events, ClickEvent{Type: TypePress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}) return ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}, true
case pointer.Leave: case pointer.Leave:
if !c.pressed { if !c.pressed {
c.pid = e.PointerID c.pid = e.PointerID
@@ -242,25 +253,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 +270,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 +300,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
@@ -309,9 +316,8 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
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:
@@ -324,14 +330,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.Dp(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,11 +346,14 @@ 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 { if axis == Horizontal {
return p.X return p.X
} else { } else {
return p.Y return p.Y
@@ -365,23 +374,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
@@ -409,7 +420,7 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
diff := e.Position.Sub(d.start) diff := e.Position.Sub(d.start)
slop := cfg.Dp(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 +429,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 +454,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")
} }
} }
+21 -21
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)
} }
@@ -92,7 +102,7 @@ func TestMouseClicks(t *testing.T) {
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,
} }
@@ -101,18 +111,8 @@ func mouseClickEvents(times ...time.Duration) []event.Event {
press := press press := press
press.Time = t press.Time = t
release := press release := press
release.Type = pointer.Release release.Kind = pointer.Release
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
}
+5 -5
View File
@@ -1,16 +1,16 @@
module gioui.org module gioui.org
go 1.19 go 1.21
require ( require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.6 gioui.org/shader v1.0.8
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 github.com/go-text/typesetting v0.1.1
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
golang.org/x/image v0.5.0 golang.org/x/image v0.5.0
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 golang.org/x/sys v0.5.0
) )
require golang.org/x/text v0.7.0 require golang.org/x/text v0.9.0
+10 -8
View File
@@ -3,11 +3,12 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8v
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/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= 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.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -28,15 +29,16 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/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-20191119224855-298f0cb1881e/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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+15 -10
View File
@@ -8,8 +8,13 @@ import (
"gioui.org/internal/f32" "gioui.org/internal/f32"
) )
type resourceCache struct { type textureCacheKey struct {
res map[interface{}]resourceCacheValue filter byte
handle any
}
type textureCache struct {
res map[textureCacheKey]resourceCacheValue
} }
type resourceCacheValue struct { type resourceCacheValue struct {
@@ -37,13 +42,13 @@ type opCacheValue struct {
keep bool keep bool
} }
func newResourceCache() *resourceCache { func newTextureCache() *textureCache {
return &resourceCache{ return &textureCache{
res: make(map[interface{}]resourceCacheValue), res: make(map[textureCacheKey]resourceCacheValue),
} }
} }
func (r *resourceCache) get(key interface{}) (resource, bool) { func (r *textureCache) get(key textureCacheKey) (resource, bool) {
v, exists := r.res[key] v, exists := r.res[key]
if !exists { if !exists {
return nil, false return nil, false
@@ -55,17 +60,17 @@ func (r *resourceCache) get(key interface{}) (resource, bool) {
return v.resource, exists return v.resource, exists
} }
func (r *resourceCache) put(key interface{}, val resource) { func (r *textureCache) put(key textureCacheKey, val resource) {
v, exists := r.res[key] v, exists := r.res[key]
if exists && v.used { if exists && v.used {
panic(fmt.Errorf("key exists, %p", key)) panic(fmt.Errorf("key exists, %v", key))
} }
v.used = true v.used = true
v.resource = val v.resource = val
r.res[key] = v 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 v.used { if v.used {
v.used = false v.used = false
@@ -77,7 +82,7 @@ func (r *resourceCache) frame() {
} }
} }
func (r *resourceCache) release() { func (r *textureCache) release() {
for _, v := range r.res { for _, v := range r.res {
v.resource.release() v.resource.release()
} }
-24
View File
@@ -1,24 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package gpu
import "testing"
func BenchmarkResourceCache(b *testing.B) {
offset := 0
const N = 100
cache := newResourceCache()
for i := 0; i < b.N; i++ {
// half are the same and half updated
for k := 0; k < N; k++ {
cache.put(offset+k, nullResource{})
}
cache.frame()
offset += N / 2
}
}
type nullResource struct{}
func (nullResource) release() {}
+3 -12
View File
@@ -93,7 +93,6 @@ type compute struct {
} }
} }
timers struct { timers struct {
profile string
t *timers t *timers
compact *timer compact *timer
render *timer render *timer
@@ -176,7 +175,6 @@ type materialUniforms struct {
type collector struct { type collector struct {
hasher maphash.Hash hasher maphash.Hash
profile bool
reader ops.Reader reader ops.Reader
states []f32.Affine2D states []f32.Affine2D
clear bool clear bool
@@ -597,7 +595,7 @@ func (g *compute) frame(target RenderTarget) error {
defer g.ctx.EndFrame() defer g.ctx.EndFrame()
t := &g.timers t := &g.timers
if g.collector.profile && t.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) { if false && t.t == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
t.t = newTimers(g.ctx) t.t = newTimers(g.ctx)
t.compact = t.t.newTimer() t.compact = t.t.newTimer()
t.render = t.t.newTimer() t.render = t.t.newTimer()
@@ -631,13 +629,13 @@ func (g *compute) frame(target RenderTarget) error {
return err return err
} }
t.compact.end() t.compact.end()
if g.collector.profile && t.t.ready() { if false && t.t.ready() {
com, ren, blit := t.compact.Elapsed, t.render.Elapsed, t.blit.Elapsed com, ren, blit := t.compact.Elapsed, t.render.Elapsed, t.blit.Elapsed
ft := com + ren + blit ft := com + ren + blit
q := 100 * time.Microsecond q := 100 * time.Microsecond
ft = ft.Round(q) ft = ft.Round(q)
com, ren, blit = com.Round(q), ren.Round(q), blit.Round(q) com, ren, blit = com.Round(q), ren.Round(q), blit.Round(q)
t.profile = fmt.Sprintf("ft:%7s com: %7s ren:%7s blit:%7s", ft, com, ren, blit) // t.profile = fmt.Sprintf("ft:%7s com: %7s ren:%7s blit:%7s", ft, com, ren, blit)
} }
return nil return nil
} }
@@ -661,10 +659,6 @@ func (g *compute) dumpAtlases() {
} }
} }
func (g *compute) Profile() string {
return g.timers.profile
}
func (g *compute) compactAllocs() error { func (g *compute) compactAllocs() error {
const ( const (
maxAllocAge = 3 maxAllocAge = 3
@@ -1656,7 +1650,6 @@ func (e *encoder) line(start, end f32.Point) {
func (c *collector) reset() { func (c *collector) reset() {
c.prevFrame, c.frame = c.frame, c.prevFrame c.prevFrame, c.frame = c.frame, c.prevFrame
c.profile = false
c.clipStates = c.clipStates[:0] c.clipStates = c.clipStates[:0]
c.transStack = c.transStack[:0] c.transStack = c.transStack[:0]
c.frame.reset() c.frame.reset()
@@ -1736,8 +1729,6 @@ func (c *collector) collect(root *op.Ops, viewport image.Point, texOps *[]textur
c.addClip(&state, fview, fview, nil, ops.Key{}, 0, 0, false) c.addClip(&state, fview, fview, nil, ops.Key{}, 0, 0, false)
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:
c.profile = true
case ops.TypeTransform: case ops.TypeTransform:
dop, push := ops.DecodeTransform(encOp.Data) dop, push := ops.DecodeTransform(encOp.Data)
if push { if push {
+326 -148
View File
@@ -44,14 +44,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 +64,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 +140,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 +172,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 interface{}
filter byte
} }
type linearGradientOpData struct { type linearGradientOpData struct {
@@ -182,6 +208,7 @@ 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],
} }
} }
@@ -222,8 +249,6 @@ func decodeLinearGradientOp(data []byte) linearGradientOpData {
} }
} }
type clipType uint8
type resource interface { type resource interface {
release() release()
} }
@@ -236,7 +261,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
@@ -273,6 +298,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 {
@@ -284,7 +312,7 @@ type gradientUniforms struct {
color2 f32color.RGBA color2 f32color.RGBA
} }
type materialType uint8 type clipType uint8
const ( const (
clipTypeNone clipType = iota clipTypeNone clipType = iota
@@ -292,6 +320,8 @@ const (
clipTypeIntersection clipTypeIntersection
) )
type materialType uint8
const ( const (
materialColor materialType = iota materialColor materialType = iota
materialLinearGradient materialLinearGradient
@@ -324,7 +354,7 @@ func NewWithDevice(d driver.Device) (GPU, error) {
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 {
@@ -364,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()
@@ -390,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,
} }
@@ -400,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
@@ -423,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.FilterLinearMipmapLinear, driver.FilterLinear, driver.BufferBindingTexture)
var minFilter, magFilter driver.TextureFilter
switch data.filter {
case filterLinear:
minFilter, magFilter = driver.FilterLinearMipmapLinear, driver.FilterLinear
case filterNearest:
minFilter, magFilter = driver.FilterNearest, driver.FilterNearest
}
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA,
data.src.Bounds().Dx(), data.src.Bounds().Dy(),
minFilter, magFilter,
driver.BufferBindingTexture,
)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -464,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 {
@@ -507,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]interface{}) (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,
@@ -530,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
} }
@@ -747,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.
@@ -760,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 = append(layers[:i], layers[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.Affine2D{}.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) {
@@ -800,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,
@@ -853,8 +985,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 {
@@ -866,6 +996,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)
@@ -906,7 +1057,7 @@ loop:
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}
} }
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
@@ -951,14 +1102,14 @@ loop:
// 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.SetTransform(t) // TODO: This call has no effect.
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]
@@ -971,6 +1122,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 {
@@ -1000,7 +1160,9 @@ 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.,
}
switch d.matType { switch d.matType {
case materialColor: case materialColor:
m.material = materialColor m.material = materialColor
@@ -1039,24 +1201,25 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
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
@@ -1069,24 +1232,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)
@@ -1102,15 +1271,19 @@ func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
Max: img.place.Pos.Add(drc.Size()), Max: img.place.Pos.Add(drc.Size()),
} }
coverScale, coverOff := texSpaceTransform(f32.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 {
@@ -1119,18 +1292,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)
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+83
View File
@@ -359,6 +359,73 @@ func TestImageRGBA(t *testing.T) {
}) })
} }
func TestImageRGBA_ScaleLinear(t *testing.T) {
run(t, func(o *op.Ops) {
w := newWindow(t, 128, 128)
defer clip.Rect{Max: image.Pt(128, 128)}.Push(o).Pop()
op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o)
im := image.NewRGBA(image.Rect(0, 0, 2, 2))
im.Set(0, 0, colornames.Red)
im.Set(1, 0, colornames.Green)
im.Set(0, 1, colornames.White)
im.Set(1, 1, colornames.Black)
op := paint.NewImageOp(im)
op.Filter = paint.FilterLinear
op.Add(o)
paint.PaintOp{}.Add(o)
if err := w.Frame(o); err != nil {
t.Error(err)
}
}, func(r result) {
r.expect(0, 0, colornames.Red)
r.expect(8, 8, colornames.Red)
// TODO: this currently seems to do srgb scaling
// instead of linear rgb scaling,
r.expect(64-4, 0, color.RGBA{R: 197, G: 87, B: 0, A: 255})
r.expect(64+4, 0, color.RGBA{R: 175, G: 98, B: 0, A: 255})
r.expect(127, 0, colornames.Green)
r.expect(127-8, 8, colornames.Green)
})
}
func TestImageRGBA_ScaleNearest(t *testing.T) {
run(t, func(o *op.Ops) {
w := newWindow(t, 128, 128)
op.Affine(f32.Affine2D{}.Scale(f32.Point{}, f32.Pt(64, 64))).Add(o)
im := image.NewRGBA(image.Rect(0, 0, 2, 2))
im.Set(0, 0, colornames.Red)
im.Set(1, 0, colornames.Green)
im.Set(0, 1, colornames.White)
im.Set(1, 1, colornames.Black)
op := paint.NewImageOp(im)
op.Filter = paint.FilterNearest
op.Add(o)
paint.PaintOp{}.Add(o)
if err := w.Frame(o); err != nil {
t.Error(err)
}
}, func(r result) {
r.expect(0, 0, colornames.Red)
r.expect(8, 8, colornames.Red)
r.expect(64-4, 0, colornames.Red)
r.expect(64+4, 0, colornames.Green)
r.expect(127, 0, colornames.Green)
r.expect(127-8, 8, colornames.Green)
})
}
func TestGapsInPath(t *testing.T) { func TestGapsInPath(t *testing.T) {
ops := new(op.Ops) ops := new(op.Ops)
var p clip.Path var p clip.Path
@@ -413,6 +480,22 @@ func TestGapsInPath(t *testing.T) {
}) })
} }
func TestOpacity(t *testing.T) {
run(t, func(ops *op.Ops) {
opc1 := paint.PushOpacity(ops, .3)
// Fill screen to exercise the glClear optimization.
paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op())
opc2 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op())
opc2.Pop()
opc1.Pop()
opc3 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{B: 255, A: 255}, clip.Ellipse(image.Rectangle{Min: image.Pt(20+20, 10), Max: image.Pt(50+64, 128)}).Op(ops))
opc3.Pop()
}, func(r result) {
})
}
// lerp calculates linear interpolation with color b and p. // lerp calculates linear interpolation with color b and p.
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA { func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
return f32color.RGBA{ return f32color.RGBA{
+25 -15
View File
@@ -30,7 +30,7 @@ type pather struct {
type coverer struct { type coverer struct {
ctx driver.Device ctx driver.Device
pipelines [3]*pipeline pipelines [2][3]*pipeline
texUniforms *coverTexUniforms texUniforms *coverTexUniforms
colUniforms *coverColUniforms colUniforms *coverColUniforms
linearGradientUniforms *coverLinearGradientUniforms linearGradientUniforms *coverLinearGradientUniforms
@@ -58,7 +58,7 @@ type coverUniforms struct {
uvCoverTransform [4]float32 uvCoverTransform [4]float32
uvTransformR1 [4]float32 uvTransformR1 [4]float32
uvTransformR2 [4]float32 uvTransformR2 [4]float32
_ float32 fbo float32
} }
type stenciler struct { type stenciler struct {
@@ -90,10 +90,10 @@ type intersectUniforms struct {
} }
type fboSet struct { type fboSet struct {
fbos []stencilFBO fbos []FBO
} }
type stencilFBO struct { type FBO struct {
size image.Point size image.Point
tex driver.Texture tex driver.Texture
} }
@@ -247,10 +247,10 @@ func newStenciler(ctx driver.Device) *stenciler {
return st return st
} }
func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) { func (s *fboSet) resize(ctx driver.Device, format driver.TextureFormat, sizes []image.Point) {
// Add fbos. // Add fbos.
for i := len(s.fbos); i < len(sizes); i++ { for i := len(s.fbos); i < len(sizes); i++ {
s.fbos = append(s.fbos, stencilFBO{}) s.fbos = append(s.fbos, FBO{})
} }
// Resize fbos. // Resize fbos.
for i, sz := range sizes { for i, sz := range sizes {
@@ -273,7 +273,7 @@ func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
if sz.X > max { if sz.X > max {
sz.X = max sz.X = max
} }
tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest, tex, err := ctx.NewTexture(format, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest,
driver.BufferBindingTexture|driver.BufferBindingFramebuffer) driver.BufferBindingTexture|driver.BufferBindingFramebuffer)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -309,7 +309,9 @@ func (p *pather) release() {
func (c *coverer) release() { func (c *coverer) release() {
for _, p := range c.pipelines { for _, p := range c.pipelines {
p.Release() for _, p := range p {
p.Release()
}
} }
} }
@@ -340,15 +342,15 @@ func (s *stenciler) beginIntersect(sizes []image.Point) {
// 8 bit coverage is enough, but OpenGL ES only supports single channel // 8 bit coverage is enough, but OpenGL ES only supports single channel
// floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if // floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
// no floating point support is available. // no floating point support is available.
s.intersections.resize(s.ctx, sizes) s.intersections.resize(s.ctx, driver.TextureFormatFloat, sizes)
} }
func (s *stenciler) cover(idx int) stencilFBO { func (s *stenciler) cover(idx int) FBO {
return s.fbos.fbos[idx] return s.fbos.fbos[idx]
} }
func (s *stenciler) begin(sizes []image.Point) { func (s *stenciler) begin(sizes []image.Point) {
s.fbos.resize(s.ctx, sizes) s.fbos.resize(s.ctx, driver.TextureFormatFloat, sizes)
} }
func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) { func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) {
@@ -375,11 +377,11 @@ func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv ima
} }
} }
func (p *pather) cover(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) { func (p *pather) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
p.coverer.cover(mat, col, col1, col2, scale, off, uvTrans, coverScale, coverOff) p.coverer.cover(mat, isFBO, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
} }
func (c *coverer) cover(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) { func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
var uniforms *coverUniforms var uniforms *coverUniforms
switch mat { switch mat {
case materialColor: case materialColor:
@@ -399,9 +401,17 @@ func (c *coverer) cover(mat materialType, col f32color.RGBA, col1, col2 f32color
c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0} c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
uniforms = &c.texUniforms.coverUniforms uniforms = &c.texUniforms.coverUniforms
} }
uniforms.fbo = 0
if isFBO {
uniforms.fbo = 1
}
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y} uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
c.pipelines[mat].UploadUniforms(c.ctx) fboIdx := 0
if isFBO {
fboIdx = 1
}
c.pipelines[fboIdx][mat].UploadUniforms(c.ctx)
c.ctx.DrawArrays(0, 4) c.ctx.DrawArrays(0, 4)
} }
+5 -7
View File
@@ -15,10 +15,9 @@ import (
) )
type Context struct { type Context struct {
disp _EGLDisplay disp _EGLDisplay
eglCtx *eglContext eglCtx *eglContext
eglSurf _EGLSurface eglSurf _EGLSurface
width, height int
} }
type eglContext struct { type eglContext struct {
@@ -62,6 +61,7 @@ func (c *Context) Release() {
eglDestroyContext(c.disp, c.eglCtx.ctx) eglDestroyContext(c.disp, c.eglCtx.ctx)
c.eglCtx = nil c.eglCtx = nil
} }
eglTerminate(c.disp)
c.disp = nilEGLDisplay c.disp = nilEGLDisplay
} }
@@ -120,11 +120,9 @@ func (c *Context) VisualID() int {
return c.eglCtx.visualID return c.eglCtx.visualID
} }
func (c *Context) CreateSurface(win NativeWindowType, width, height int) error { func (c *Context) CreateSurface(win NativeWindowType) error {
eglSurf, err := createSurface(c.disp, c.eglCtx, win) eglSurf, err := createSurface(c.disp, c.eglCtx, win)
c.eglSurf = eglSurf c.eglSurf = eglSurf
c.width = width
c.height = height
return err return err
} }
+1 -1
View File
@@ -665,7 +665,7 @@ func (f *Functions) load(forceES bool) error {
case runtime.GOOS == "android": case runtime.GOOS == "android":
libNames = []string{"libGLESv2.so", "libGLESv3.so"} libNames = []string{"libGLESv2.so", "libGLESv3.so"}
default: default:
libNames = []string{"libGLESv2.so.2"} libNames = []string{"libGLESv2.so.2", "libGLESv2.so.3.0"}
} }
for _, lib := range libNames { for _, lib := range libNames {
if h := dlopen(lib); h != nil { if h := dlopen(lib); h != nil {
+3
View File
@@ -361,6 +361,9 @@ func (c *Functions) GetProgrami(p Program, pname Enum) int {
} }
func (c *Functions) GetProgramInfoLog(p Program) string { func (c *Functions) GetProgramInfoLog(p Program) string {
n := c.GetProgrami(p, INFO_LOG_LENGTH) n := c.GetProgrami(p, INFO_LOG_LENGTH)
if n == 0 {
return ""
}
buf := make([]byte, n) buf := make([]byte, n)
syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0)
return string(buf) return string(buf)
+79 -86
View File
@@ -14,14 +14,25 @@ import (
type Ops struct { type Ops struct {
// version is incremented at each Reset. // version is incremented at each Reset.
version int version uint32
// data contains the serialized operations. // data contains the serialized operations.
data []byte data []byte
// refs hold external references for operations. // refs hold external references for operations.
refs []interface{} refs []interface{}
// stringRefs provides space for string references, pointers to which will
// be stored in refs. Storing a string directly in refs would cause a heap
// allocation, to store the string header in an interface value. The backing
// array of stringRefs, on the other hand, gets reused between calls to
// reset, making string references free on average.
//
// Appending to stringRefs might reallocate the backing array, which will
// leave pointers to the old array in refs. This temporarily causes a slight
// increase in memory usage, but this, too, amortizes away as the capacity
// of stringRefs approaches its stable maximum.
stringRefs []string
// nextStateID is the id allocated for the next // nextStateID is the id allocated for the next
// StateOp. // StateOp.
nextStateID int nextStateID uint32
// multipOp indicates a multi-op such as clip.Path is being added. // multipOp indicates a multi-op such as clip.Path is being added.
multipOp bool multipOp bool
@@ -40,31 +51,23 @@ const (
TypeMacro OpType = iota + firstOpIndex TypeMacro OpType = iota + firstOpIndex
TypeCall TypeCall
TypeDefer TypeDefer
TypePushTransform
TypeTransform TypeTransform
TypePopTransform TypePopTransform
TypeInvalidate TypePushOpacity
TypePopOpacity
TypeImage TypeImage
TypePaint TypePaint
TypeColor TypeColor
TypeLinearGradient TypeLinearGradient
TypePass TypePass
TypePopPass TypePopPass
TypePointerInput TypeInput
TypeClipboardRead TypeKeyInputHint
TypeClipboardWrite
TypeSource
TypeTarget
TypeOffer
TypeKeyInput
TypeKeyFocus
TypeKeySoftKeyboard
TypeSave TypeSave
TypeLoad TypeLoad
TypeAux TypeAux
TypeClip TypeClip
TypePopClip TypePopClip
TypeProfile
TypeCursor TypeCursor
TypePath TypePath
TypeStroke TypeStroke
@@ -72,30 +75,28 @@ const (
TypeSemanticDesc TypeSemanticDesc
TypeSemanticClass TypeSemanticClass
TypeSemanticSelected TypeSemanticSelected
TypeSemanticDisabled TypeSemanticEnabled
TypeSnippet
TypeSelection
TypeActionInput TypeActionInput
) )
type StackID struct { type StackID struct {
id int id uint32
prev int prev uint32
} }
// StateOp represents a saved operation snapshot to be restored // StateOp represents a saved operation snapshot to be restored
// later. // later.
type StateOp struct { type StateOp struct {
id int id uint32
macroID int macroID uint32
ops *Ops ops *Ops
} }
// stack tracks the integer identities of stack operations to ensure correct // stack tracks the integer identities of stack operations to ensure correct
// pairing of their push and pop methods. // pairing of their push and pop methods.
type stack struct { type stack struct {
currentID int currentID uint32
nextID int nextID uint32
} }
type StackKind uint8 type StackKind uint8
@@ -111,6 +112,7 @@ const (
ClipStack StackKind = iota ClipStack StackKind = iota
TransStack TransStack
PassStack PassStack
OpacityStack
_StackKind _StackKind
) )
@@ -124,31 +126,24 @@ const (
TypeMacroLen = 1 + 4 + 4 TypeMacroLen = 1 + 4 + 4
TypeCallLen = 1 + 4 + 4 + 4 + 4 TypeCallLen = 1 + 4 + 4 + 4 + 4
TypeDeferLen = 1 TypeDeferLen = 1
TypePushTransformLen = 1 + 4*6
TypeTransformLen = 1 + 1 + 4*6 TypeTransformLen = 1 + 1 + 4*6
TypePopTransformLen = 1 TypePopTransformLen = 1
TypePushOpacityLen = 1 + 4
TypePopOpacityLen = 1
TypeRedrawLen = 1 + 8 TypeRedrawLen = 1 + 8
TypeImageLen = 1 TypeImageLen = 1 + 1
TypePaintLen = 1 TypePaintLen = 1
TypeColorLen = 1 + 4 TypeColorLen = 1 + 4
TypeLinearGradientLen = 1 + 8*2 + 4*2 TypeLinearGradientLen = 1 + 8*2 + 4*2
TypePassLen = 1 TypePassLen = 1
TypePopPassLen = 1 TypePopPassLen = 1
TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4 TypeInputLen = 1
TypeClipboardReadLen = 1 TypeKeyInputHintLen = 1 + 1
TypeClipboardWriteLen = 1
TypeSourceLen = 1
TypeTargetLen = 1
TypeOfferLen = 1
TypeKeyInputLen = 1 + 1
TypeKeyFocusLen = 1 + 1
TypeKeySoftKeyboardLen = 1 + 1
TypeSaveLen = 1 + 4 TypeSaveLen = 1 + 4
TypeLoadLen = 1 + 4 TypeLoadLen = 1 + 4
TypeAuxLen = 1 TypeAuxLen = 1
TypeClipLen = 1 + 4*4 + 1 + 1 TypeClipLen = 1 + 4*4 + 1 + 1
TypePopClipLen = 1 TypePopClipLen = 1
TypeProfileLen = 1
TypeCursorLen = 2 TypeCursorLen = 2
TypePathLen = 8 + 1 TypePathLen = 8 + 1
TypeStrokeLen = 1 + 4 TypeStrokeLen = 1 + 4
@@ -156,9 +151,7 @@ const (
TypeSemanticDescLen = 1 TypeSemanticDescLen = 1
TypeSemanticClassLen = 2 TypeSemanticClassLen = 2
TypeSemanticSelectedLen = 2 TypeSemanticSelectedLen = 2
TypeSemanticDisabledLen = 2 TypeSemanticEnabledLen = 2
TypeSnippetLen = 1 + 4 + 4
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
TypeActionInputLen = 1 + 1 TypeActionInputLen = 1 + 1
) )
@@ -183,8 +176,12 @@ func Reset(o *Ops) {
for i := range o.refs { for i := range o.refs {
o.refs[i] = nil o.refs[i] = nil
} }
for i := range o.stringRefs {
o.stringRefs[i] = ""
}
o.data = o.data[:0] o.data = o.data[:0]
o.refs = o.refs[:0] o.refs = o.refs[:0]
o.stringRefs = o.stringRefs[:0]
o.nextStateID = 0 o.nextStateID = 0
o.version++ o.version++
} }
@@ -248,11 +245,11 @@ func AddCall(o *Ops, callOps *Ops, pc PC, end PC) {
bo.PutUint32(data[13:], uint32(end.refs)) bo.PutUint32(data[13:], uint32(end.refs))
} }
func PushOp(o *Ops, kind StackKind) (StackID, int) { func PushOp(o *Ops, kind StackKind) (StackID, uint32) {
return o.stacks[kind].push(), o.macroStack.currentID return o.stacks[kind].push(), o.macroStack.currentID
} }
func PopOp(o *Ops, kind StackKind, sid StackID, macroID int) { func PopOp(o *Ops, kind StackKind, sid StackID, macroID uint32) {
if o.macroStack.currentID != macroID { if o.macroStack.currentID != macroID {
panic("stack push and pop must not cross macro boundary") panic("stack push and pop must not cross macro boundary")
} }
@@ -265,12 +262,26 @@ func Write1(o *Ops, n int, ref1 interface{}) []byte {
return o.data[len(o.data)-n:] return o.data[len(o.data)-n:]
} }
func Write1String(o *Ops, n int, ref1 string) []byte {
o.data = append(o.data, make([]byte, n)...)
o.stringRefs = append(o.stringRefs, ref1)
o.refs = append(o.refs, &o.stringRefs[len(o.stringRefs)-1])
return o.data[len(o.data)-n:]
}
func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte { func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte {
o.data = append(o.data, make([]byte, n)...) o.data = append(o.data, make([]byte, n)...)
o.refs = append(o.refs, ref1, ref2) o.refs = append(o.refs, ref1, ref2)
return o.data[len(o.data)-n:] return o.data[len(o.data)-n:]
} }
func Write2String(o *Ops, n int, ref1 interface{}, ref2 string) []byte {
o.data = append(o.data, make([]byte, n)...)
o.stringRefs = append(o.stringRefs, ref2)
o.refs = append(o.refs, ref1, &o.stringRefs[len(o.stringRefs)-1])
return o.data[len(o.data)-n:]
}
func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte { func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
o.data = append(o.data, make([]byte, n)...) o.data = append(o.data, make([]byte, n)...)
o.refs = append(o.refs, ref1, ref2, ref3) o.refs = append(o.refs, ref1, ref2, ref3)
@@ -278,7 +289,7 @@ func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
} }
func PCFor(o *Ops) PC { func PCFor(o *Ops) PC {
return PC{data: len(o.data), refs: len(o.refs)} return PC{data: uint32(len(o.data)), refs: uint32(len(o.refs))}
} }
func (s *stack) push() StackID { func (s *stack) push() StackID {
@@ -354,6 +365,14 @@ func DecodeTransform(data []byte) (t f32.Affine2D, push bool) {
return f32.NewAffine2D(a, b, c, d, e, f), push return f32.NewAffine2D(a, b, c, d, e, f), push
} }
func DecodeOpacity(data []byte) float32 {
if OpType(data[0]) != TypePushOpacity {
panic("invalid op")
}
bo := binary.LittleEndian
return math.Float32frombits(bo.Uint32(data[1:]))
}
// DecodeSave decodes the state id of a save op. // DecodeSave decodes the state id of a save op.
func DecodeSave(data []byte) int { func DecodeSave(data []byte) int {
if OpType(data[0]) != TypeSave { if OpType(data[0]) != TypeSave {
@@ -381,31 +400,23 @@ var opProps = [0x100]opProp{
TypeMacro: {Size: TypeMacroLen, NumRefs: 0}, TypeMacro: {Size: TypeMacroLen, NumRefs: 0},
TypeCall: {Size: TypeCallLen, NumRefs: 1}, TypeCall: {Size: TypeCallLen, NumRefs: 1},
TypeDefer: {Size: TypeDeferLen, NumRefs: 0}, TypeDefer: {Size: TypeDeferLen, NumRefs: 0},
TypePushTransform: {Size: TypePushTransformLen, NumRefs: 0},
TypeTransform: {Size: TypeTransformLen, NumRefs: 0}, TypeTransform: {Size: TypeTransformLen, NumRefs: 0},
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0}, TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0}, TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
TypeImage: {Size: TypeImageLen, NumRefs: 2}, TypeImage: {Size: TypeImageLen, NumRefs: 2},
TypePaint: {Size: TypePaintLen, NumRefs: 0}, TypePaint: {Size: TypePaintLen, NumRefs: 0},
TypeColor: {Size: TypeColorLen, NumRefs: 0}, TypeColor: {Size: TypeColorLen, NumRefs: 0},
TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0}, TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0},
TypePass: {Size: TypePassLen, NumRefs: 0}, TypePass: {Size: TypePassLen, NumRefs: 0},
TypePopPass: {Size: TypePopPassLen, NumRefs: 0}, TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1}, TypeInput: {Size: TypeInputLen, NumRefs: 1},
TypeClipboardRead: {Size: TypeClipboardReadLen, NumRefs: 1}, TypeKeyInputHint: {Size: TypeKeyInputHintLen, NumRefs: 1},
TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1},
TypeSource: {Size: TypeSourceLen, NumRefs: 2},
TypeTarget: {Size: TypeTargetLen, NumRefs: 2},
TypeOffer: {Size: TypeOfferLen, NumRefs: 3},
TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2},
TypeKeyFocus: {Size: TypeKeyFocusLen, NumRefs: 1},
TypeKeySoftKeyboard: {Size: TypeKeySoftKeyboardLen, NumRefs: 0},
TypeSave: {Size: TypeSaveLen, NumRefs: 0}, TypeSave: {Size: TypeSaveLen, NumRefs: 0},
TypeLoad: {Size: TypeLoadLen, NumRefs: 0}, TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
TypeAux: {Size: TypeAuxLen, NumRefs: 0}, TypeAux: {Size: TypeAuxLen, NumRefs: 0},
TypeClip: {Size: TypeClipLen, NumRefs: 0}, TypeClip: {Size: TypeClipLen, NumRefs: 0},
TypePopClip: {Size: TypePopClipLen, NumRefs: 0}, TypePopClip: {Size: TypePopClipLen, NumRefs: 0},
TypeProfile: {Size: TypeProfileLen, NumRefs: 1},
TypeCursor: {Size: TypeCursorLen, NumRefs: 0}, TypeCursor: {Size: TypeCursorLen, NumRefs: 0},
TypePath: {Size: TypePathLen, NumRefs: 0}, TypePath: {Size: TypePathLen, NumRefs: 0},
TypeStroke: {Size: TypeStrokeLen, NumRefs: 0}, TypeStroke: {Size: TypeStrokeLen, NumRefs: 0},
@@ -413,23 +424,21 @@ var opProps = [0x100]opProp{
TypeSemanticDesc: {Size: TypeSemanticDescLen, NumRefs: 1}, TypeSemanticDesc: {Size: TypeSemanticDescLen, NumRefs: 1},
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0}, TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0}, TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
TypeSemanticDisabled: {Size: TypeSemanticDisabledLen, NumRefs: 0}, TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0},
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0}, TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
} }
func (t OpType) props() (size, numRefs int) { func (t OpType) props() (size, numRefs uint32) {
v := opProps[t] v := opProps[t]
return int(v.Size), int(v.NumRefs) return uint32(v.Size), uint32(v.NumRefs)
} }
func (t OpType) Size() int { func (t OpType) Size() uint32 {
return int(opProps[t].Size) return uint32(opProps[t].Size)
} }
func (t OpType) NumRefs() int { func (t OpType) NumRefs() uint32 {
return int(opProps[t].NumRefs) return uint32(opProps[t].NumRefs)
} }
func (t OpType) String() string { func (t OpType) String() string {
@@ -440,14 +449,14 @@ func (t OpType) String() string {
return "Call" return "Call"
case TypeDefer: case TypeDefer:
return "Defer" return "Defer"
case TypePushTransform:
return "PushTransform"
case TypeTransform: case TypeTransform:
return "Transform" return "Transform"
case TypePopTransform: case TypePopTransform:
return "PopTransform" return "PopTransform"
case TypeInvalidate: case TypePushOpacity:
return "Invalidate" return "PushOpacity"
case TypePopOpacity:
return "PopOpacity"
case TypeImage: case TypeImage:
return "Image" return "Image"
case TypePaint: case TypePaint:
@@ -460,24 +469,10 @@ func (t OpType) String() string {
return "Pass" return "Pass"
case TypePopPass: case TypePopPass:
return "PopPass" return "PopPass"
case TypePointerInput: case TypeInput:
return "PointerInput" return "Input"
case TypeClipboardRead: case TypeKeyInputHint:
return "ClipboardRead" return "KeyInputHint"
case TypeClipboardWrite:
return "ClipboardWrite"
case TypeSource:
return "Source"
case TypeTarget:
return "Target"
case TypeOffer:
return "Offer"
case TypeKeyInput:
return "KeyInput"
case TypeKeyFocus:
return "KeyFocus"
case TypeKeySoftKeyboard:
return "KeySoftKeyboard"
case TypeSave: case TypeSave:
return "Save" return "Save"
case TypeLoad: case TypeLoad:
@@ -488,8 +483,6 @@ func (t OpType) String() string {
return "Clip" return "Clip"
case TypePopClip: case TypePopClip:
return "PopClip" return "PopClip"
case TypeProfile:
return "Profile"
case TypeCursor: case TypeCursor:
return "Cursor" return "Cursor"
case TypePath: case TypePath:
+13 -13
View File
@@ -26,8 +26,8 @@ type EncodedOp struct {
// Key is a unique key for a given op. // Key is a unique key for a given op.
type Key struct { type Key struct {
ops *Ops ops *Ops
pc int pc uint32
version int version uint32
} }
// Shadow of op.MacroOp. // Shadow of op.MacroOp.
@@ -39,8 +39,8 @@ type macroOp struct {
// PC is an instruction counter for an operation list. // PC is an instruction counter for an operation list.
type PC struct { type PC struct {
data int data uint32
refs int refs uint32
} }
type macro struct { type macro struct {
@@ -128,7 +128,7 @@ func (r *Reader) Decode() (EncodedOp, bool) {
if nrefs != 1 { if nrefs != 1 {
panic("internal error: unexpected number of macro refs") panic("internal error: unexpected number of macro refs")
} }
deferData := Write1(&r.deferOps, n, refs[0]) deferData := Write1(&r.deferOps, int(n), refs[0])
copy(deferData, data) copy(deferData, data)
r.pc.data += n r.pc.data += n
r.pc.refs += nrefs r.pc.refs += nrefs
@@ -154,8 +154,8 @@ func (r *Reader) Decode() (EncodedOp, bool) {
r.pc = op.endpc r.pc = op.endpc
} else { } else {
// Treat an incomplete macro as containing all remaining ops. // Treat an incomplete macro as containing all remaining ops.
r.pc.data = len(r.ops.data) r.pc.data = uint32(len(r.ops.data))
r.pc.refs = len(r.ops.refs) r.pc.refs = uint32(len(r.ops.refs))
} }
continue continue
} }
@@ -171,8 +171,8 @@ func (op *opMacroDef) decode(data []byte) {
} }
bo := binary.LittleEndian bo := binary.LittleEndian
data = data[:TypeMacroLen] data = data[:TypeMacroLen]
op.endpc.data = int(int32(bo.Uint32(data[1:]))) op.endpc.data = bo.Uint32(data[1:])
op.endpc.refs = int(int32(bo.Uint32(data[5:]))) op.endpc.refs = bo.Uint32(data[5:])
} }
func (m *macroOp) decode(data []byte, refs []interface{}) { func (m *macroOp) decode(data []byte, refs []interface{}) {
@@ -183,8 +183,8 @@ func (m *macroOp) decode(data []byte, refs []interface{}) {
data = data[:TypeCallLen] data = data[:TypeCallLen]
m.ops = refs[0].(*Ops) m.ops = refs[0].(*Ops)
m.start.data = int(int32(bo.Uint32(data[1:]))) m.start.data = bo.Uint32(data[1:])
m.start.refs = int(int32(bo.Uint32(data[5:]))) m.start.refs = bo.Uint32(data[5:])
m.end.data = int(int32(bo.Uint32(data[9:]))) m.end.data = bo.Uint32(data[9:])
m.end.refs = int(int32(bo.Uint32(data[13:]))) m.end.refs = bo.Uint32(data[13:])
} }
+3
View File
@@ -327,6 +327,9 @@ func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) } func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
func normPt(p f32.Point, l float32) f32.Point { func normPt(p f32.Point, l float32) f32.Point {
if (p.X == 0 && p.Y == l) || (p.Y == 0 && p.X == l) {
return f32.Point{X: p.X, Y: p.Y}
}
d := math.Hypot(float64(p.X), float64(p.Y)) d := math.Hypot(float64(p.X), float64(p.Y))
l64 := float64(l) l64 := float64(l)
if math.Abs(d-l64) < 1e-10 { if math.Abs(d-l64) < 1e-10 {
+11 -24
View File
@@ -3,35 +3,22 @@
package clipboard package clipboard
import ( import (
"gioui.org/internal/ops" "io"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/op"
) )
// Event is generated when the clipboard content is requested. // WriteCmd copies Text to the clipboard.
type Event struct { type WriteCmd struct {
Text string Type string
Data io.ReadCloser
} }
// ReadOp requests the text of the clipboard, delivered to // ReadCmd requests the text of the clipboard, delivered to
// the current handler through an Event. // the handler through an [io/transfer.DataEvent].
type ReadOp struct { type ReadCmd struct {
Tag event.Tag Tag event.Tag
} }
// WriteOp copies Text to the clipboard. func (WriteCmd) ImplementsCommand() {}
type WriteOp struct { func (ReadCmd) ImplementsCommand() {}
Text string
}
func (h ReadOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeClipboardReadLen, h.Tag)
data[0] = byte(ops.TypeClipboardRead)
}
func (h WriteOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeClipboardWriteLen, &h.Text)
data[0] = byte(ops.TypeClipboardWrite)
}
func (Event) ImplementsEvent() {}
+20 -34
View File
@@ -1,41 +1,12 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
/* // Package event contains types for event handling.
Package event contains the types for event handling.
The Queue interface is the protocol for receiving external events.
For example:
var queue event.Queue = ...
for _, e := range queue.Events(h) {
switch e.(type) {
...
}
}
In general, handlers must be declared before events become
available. Other packages such as pointer and key provide
the means for declaring handlers for specific event types.
The following example declares a handler ready for key input:
import gioui.org/io/key
ops := new(op.Ops)
var h *Handler = ...
key.InputOp{Tag: h, Filter: ...}.Add(ops)
*/
package event package event
// Queue maps an event handler key to the events import (
// available to the handler. "gioui.org/internal/ops"
type Queue interface { "gioui.org/op"
// Events returns the available events for an )
// event handler tag.
Events(t Tag) []Event
}
// Tag is the stable identifier for an event handler. // Tag is the stable identifier for an event handler.
// For a handler h, the tag is typically &h. // For a handler h, the tag is typically &h.
@@ -45,3 +16,18 @@ type Tag interface{}
type Event interface { type Event interface {
ImplementsEvent() ImplementsEvent()
} }
// Filter represents a filter for [Event] types.
type Filter interface {
ImplementsFilter()
}
// Op declares a tag for input routing at the current transformation
// and clip area hierarchy. It panics if tag is nil.
func Op(o *op.Ops, tag Tag) {
if tag == nil {
panic("Tag must be non-nil")
}
data := ops.Write1(&o.Internal, ops.TypeInputLen, tag)
data[0] = byte(ops.TypeInput)
}
+72
View File
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"io"
"gioui.org/io/clipboard"
"gioui.org/io/event"
)
// clipboardState contains the state for clipboard event routing.
type clipboardState struct {
receivers []event.Tag
}
type clipboardQueue struct {
// request avoid read clipboard every frame while waiting.
requested bool
mime string
text []byte
}
// WriteClipboard returns the most recent data to be copied
// to the clipboard, if any.
func (q *clipboardQueue) WriteClipboard() (mime string, content []byte, ok bool) {
if q.text == nil {
return "", nil, false
}
content = q.text
q.text = nil
return q.mime, content, true
}
// ClipboardRequested reports if any new handler is waiting
// to read the clipboard.
func (q *clipboardQueue) ClipboardRequested(state clipboardState) bool {
req := len(state.receivers) > 0 && q.requested
q.requested = false
return req
}
func (q *clipboardQueue) Push(state clipboardState, e event.Event) (clipboardState, []taggedEvent) {
var evts []taggedEvent
for _, r := range state.receivers {
evts = append(evts, taggedEvent{tag: r, event: e})
}
state.receivers = nil
return state, evts
}
func (q *clipboardQueue) ProcessWriteClipboard(req clipboard.WriteCmd) {
defer req.Data.Close()
content, err := io.ReadAll(req.Data)
if err != nil {
return
}
q.mime = req.Type
q.text = content
}
func (q *clipboardQueue) ProcessReadClipboard(state clipboardState, tag event.Tag) clipboardState {
for _, k := range state.receivers {
if k == tag {
return state
}
}
n := len(state.receivers)
state.receivers = append(state.receivers[:n:n], tag)
q.requested = true
return state
}
+125
View File
@@ -0,0 +1,125 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"io"
"strings"
"testing"
"gioui.org/io/clipboard"
"gioui.org/io/transfer"
"gioui.org/op"
)
func TestClipboardDuplicateEvent(t *testing.T) {
ops, r, handlers := new(op.Ops), new(Router), make([]int, 2)
// Both must receive the event once.
r.Source().Execute(clipboard.ReadCmd{Tag: &handlers[0]})
r.Source().Execute(clipboard.ReadCmd{Tag: &handlers[1]})
event := transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader("Test"))
},
}
r.Queue(event)
for i := range handlers {
f := transfer.TargetFilter{Target: &handlers[i], Type: "application/text"}
assertEventTypeSequence(t, events(r, -1, f), transfer.DataEvent{})
}
assertClipboardReadCmd(t, r, 0)
r.Source().Execute(clipboard.ReadCmd{Tag: &handlers[0]})
r.Frame(ops)
// No ClipboardEvent sent
assertClipboardReadCmd(t, r, 1)
for i := range handlers {
f := transfer.TargetFilter{Target: &handlers[i]}
assertEventTypeSequence(t, events(r, -1, f))
}
}
func TestQueueProcessReadClipboard(t *testing.T) {
ops, r, handler := new(op.Ops), new(Router), make([]int, 2)
// Request read
r.Source().Execute(clipboard.ReadCmd{Tag: &handler[0]})
assertClipboardReadCmd(t, r, 1)
ops.Reset()
for i := 0; i < 3; i++ {
// No ReadCmd
// One receiver must still wait for response
r.Frame(ops)
assertClipboardReadDuplicated(t, r, 1)
}
// Send the clipboard event
event := transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader("Text 2"))
},
}
r.Queue(event)
assertEventTypeSequence(t, events(r, -1, transfer.TargetFilter{Target: &handler[0], Type: "application/text"}), transfer.DataEvent{})
assertClipboardReadCmd(t, r, 0)
}
func TestQueueProcessWriteClipboard(t *testing.T) {
r := new(Router)
const mime = "application/text"
r.Source().Execute(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 1"))})
assertClipboardWriteCmd(t, r, mime, "Write 1")
assertClipboardWriteCmd(t, r, "", "")
r.Source().Execute(clipboard.WriteCmd{Type: mime, Data: io.NopCloser(strings.NewReader("Write 2"))})
assertClipboardReadCmd(t, r, 0)
assertClipboardWriteCmd(t, r, mime, "Write 2")
}
func assertClipboardReadCmd(t *testing.T, router *Router, expected int) {
t.Helper()
if got := len(router.state().receivers); got != expected {
t.Errorf("unexpected %d receivers, got %d", expected, got)
}
if router.ClipboardRequested() != (expected > 0) {
t.Error("missing requests")
}
}
func assertClipboardReadDuplicated(t *testing.T, router *Router, expected int) {
t.Helper()
if len(router.state().receivers) != expected {
t.Error("receivers removed")
}
if router.ClipboardRequested() != false {
t.Error("duplicated requests")
}
}
func assertClipboardWriteCmd(t *testing.T, router *Router, mimeExp, expected string) {
t.Helper()
if (router.cqueue.text != nil) != (expected != "") {
t.Error("text not defined")
}
mime, text, ok := router.cqueue.WriteClipboard()
if ok != (expected != "") {
t.Error("duplicated requests")
}
if string(mime) != mimeExp {
t.Errorf("got MIME type %s, expected %s", mime, mimeExp)
}
if string(text) != expected {
t.Errorf("got text %s, expected %s", text, expected)
}
}
+15
View File
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Unlicense OR MIT
/*
Package input implements input routing and tracking of interface
state for a window.
The [Source] is the interface between the window and the widgets
of a user interface and is exposed by [gioui.org/app.FrameEvent]
received from windows.
The [Router] is used by [gioui.org/app.Window] to track window state and route
events from the platform to event handlers. It is otherwise only
useful for using Gio with external window implementations.
*/
package input
+364
View File
@@ -0,0 +1,364 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"image"
"sort"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/key"
)
// EditorState represents the state of an editor needed by input handlers.
type EditorState struct {
Selection struct {
Transform f32.Affine2D
key.Range
key.Caret
}
Snippet key.Snippet
}
type TextInputState uint8
type keyQueue struct {
order []event.Tag
dirOrder []dirFocusEntry
hint key.InputHint
}
// keyState is the input state related to key events.
type keyState struct {
focus event.Tag
state TextInputState
content EditorState
}
type keyHandler struct {
// visible will be true if the InputOp is present
// in the current frame.
visible bool
// reset tracks whether the handler has seen a
// focus reset.
reset bool
hint key.InputHint
orderPlusOne int
dirOrder int
trans f32.Affine2D
}
type keyFilter []key.Filter
type dirFocusEntry struct {
tag event.Tag
row int
area int
bounds image.Rectangle
}
const (
TextInputKeep TextInputState = iota
TextInputClose
TextInputOpen
)
func (k *keyHandler) inputHint(hint key.InputHint) {
k.hint = hint
}
// InputState returns the input state and returns a state
// reset to [TextInputKeep].
func (s keyState) InputState() (keyState, TextInputState) {
state := s.state
s.state = TextInputKeep
return s, state
}
// InputHint returns the input hint from the focused handler and whether it was
// changed since the last call.
func (q *keyQueue) InputHint(handlers map[event.Tag]*handler, state keyState) (key.InputHint, bool) {
focused, ok := handlers[state.focus]
if !ok {
return q.hint, false
}
old := q.hint
q.hint = focused.key.hint
return q.hint, old != q.hint
}
func (k *keyHandler) Reset() {
k.visible = false
k.orderPlusOne = 0
k.hint = key.HintAny
}
func (q *keyQueue) Reset() {
q.order = q.order[:0]
q.dirOrder = q.dirOrder[:0]
}
func (k *keyHandler) ResetEvent() (event.Event, bool) {
if k.reset {
return nil, false
}
k.reset = true
return key.FocusEvent{Focus: false}, true
}
func (q *keyQueue) Frame(handlers map[event.Tag]*handler, state keyState) keyState {
if state.focus != nil {
if h, ok := handlers[state.focus]; !ok || !h.filter.focusable || !h.key.visible {
// Remove focus from the handler that is no longer focusable.
state.focus = nil
state.state = TextInputClose
}
}
q.updateFocusLayout(handlers)
return state
}
// updateFocusLayout partitions input handlers handlers into rows
// for directional focus moves.
//
// The approach is greedy: pick the topmost handler and create a row
// containing it. Then, extend the handler bounds to a horizontal beam
// and add to the row every handler whose center intersect it. Repeat
// until no handlers remain.
func (q *keyQueue) updateFocusLayout(handlers map[event.Tag]*handler) {
order := q.dirOrder
// Sort by ascending y position.
sort.SliceStable(order, func(i, j int) bool {
return order[i].bounds.Min.Y < order[j].bounds.Min.Y
})
row := 0
for len(order) > 0 {
h := &order[0]
h.row = row
bottom := h.bounds.Max.Y
end := 1
for ; end < len(order); end++ {
h := &order[end]
center := (h.bounds.Min.Y + h.bounds.Max.Y) / 2
if center > bottom {
break
}
h.row = row
}
// Sort row by ascending x position.
sort.SliceStable(order[:end], func(i, j int) bool {
return order[i].bounds.Min.X < order[j].bounds.Min.X
})
order = order[end:]
row++
}
for i, o := range q.dirOrder {
handlers[o.tag].key.dirOrder = i
}
}
// MoveFocus attempts to move the focus in the direction of dir.
func (q *keyQueue) MoveFocus(handlers map[event.Tag]*handler, state keyState, dir key.FocusDirection) (keyState, []taggedEvent) {
if len(q.dirOrder) == 0 {
return state, nil
}
order := 0
if state.focus != nil {
order = handlers[state.focus].key.dirOrder
}
focus := q.dirOrder[order]
switch dir {
case key.FocusForward, key.FocusBackward:
if len(q.order) == 0 {
break
}
order := 0
if dir == key.FocusBackward {
order = -1
}
if state.focus != nil {
order = handlers[state.focus].key.orderPlusOne - 1
if dir == key.FocusForward {
order++
} else {
order--
}
}
order = (order + len(q.order)) % len(q.order)
return q.Focus(handlers, state, q.order[order])
case key.FocusRight, key.FocusLeft:
next := order
if state.focus != nil {
next = order + 1
if dir == key.FocusLeft {
next = order - 1
}
}
if 0 <= next && next < len(q.dirOrder) {
newFocus := q.dirOrder[next]
if newFocus.row == focus.row {
return q.Focus(handlers, state, newFocus.tag)
}
}
case key.FocusUp, key.FocusDown:
delta := +1
if dir == key.FocusUp {
delta = -1
}
nextRow := 0
if state.focus != nil {
nextRow = focus.row + delta
}
var closest event.Tag
dist := int(1e6)
center := (focus.bounds.Min.X + focus.bounds.Max.X) / 2
loop:
for 0 <= order && order < len(q.dirOrder) {
next := q.dirOrder[order]
switch next.row {
case nextRow:
nextCenter := (next.bounds.Min.X + next.bounds.Max.X) / 2
d := center - nextCenter
if d < 0 {
d = -d
}
if d > dist {
break loop
}
dist = d
closest = next.tag
case nextRow + delta:
break loop
}
order += delta
}
if closest != nil {
return q.Focus(handlers, state, closest)
}
}
return state, nil
}
func (q *keyQueue) BoundsFor(k *keyHandler) image.Rectangle {
order := k.dirOrder
return q.dirOrder[order].bounds
}
func (q *keyQueue) AreaFor(k *keyHandler) int {
order := k.dirOrder
return q.dirOrder[order].area
}
func (k *keyFilter) Matches(focus event.Tag, e key.Event, system bool) bool {
for _, f := range *k {
if keyFilterMatch(focus, f, e, system) {
return true
}
}
return false
}
func keyFilterMatch(focus event.Tag, f key.Filter, e key.Event, system bool) bool {
if f.Focus != nil && f.Focus != focus {
return false
}
if (f.Name != "" || system) && f.Name != e.Name {
return false
}
if e.Modifiers&f.Required != f.Required {
return false
}
if e.Modifiers&^(f.Required|f.Optional) != 0 {
return false
}
return true
}
func (q *keyQueue) Focus(handlers map[event.Tag]*handler, state keyState, focus event.Tag) (keyState, []taggedEvent) {
if focus == state.focus {
return state, nil
}
state.content = EditorState{}
var evts []taggedEvent
if state.focus != nil {
evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: false}})
}
state.focus = focus
if state.focus != nil {
evts = append(evts, taggedEvent{tag: state.focus, event: key.FocusEvent{Focus: true}})
}
if state.focus == nil || state.state == TextInputKeep {
state.state = TextInputClose
}
return state, evts
}
func (s keyState) softKeyboard(show bool) keyState {
if show {
s.state = TextInputOpen
} else {
s.state = TextInputClose
}
return s
}
func (k *keyFilter) Add(f key.Filter) {
for _, f2 := range *k {
if f == f2 {
return
}
}
*k = append(*k, f)
}
func (k *keyFilter) Merge(k2 keyFilter) {
*k = append(*k, k2...)
}
func (q *keyQueue) inputOp(tag event.Tag, state *keyHandler, t f32.Affine2D, area int, bounds image.Rectangle) {
state.visible = true
if state.orderPlusOne == 0 {
state.orderPlusOne = len(q.order) + 1
q.order = append(q.order, tag)
q.dirOrder = append(q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds})
}
state.trans = t
}
func (q *keyQueue) setSelection(state keyState, req key.SelectionCmd) keyState {
if req.Tag != state.focus {
return state
}
state.content.Selection.Range = req.Range
state.content.Selection.Caret = req.Caret
return state
}
func (q *keyQueue) editorState(handlers map[event.Tag]*handler, state keyState) EditorState {
s := state.content
if f := state.focus; f != nil {
s.Selection.Transform = handlers[f].key.trans
}
return s
}
func (q *keyQueue) setSnippet(state keyState, req key.SnippetCmd) keyState {
if req.Tag == state.focus {
state.content.Snippet = req.Snippet
}
return state
}
func (t TextInputState) String() string {
switch t {
case TextInputKeep:
return "Keep"
case TextInputClose:
return "Close"
case TextInputOpen:
return "Open"
default:
panic("unexpected value")
}
}
+332
View File
@@ -0,0 +1,332 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"image"
"testing"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/op"
"gioui.org/op/clip"
)
func TestAllMatchKeyFilter(t *testing.T) {
r := new(Router)
r.Event(key.Filter{})
ke := key.Event{Name: "A"}
r.Queue(ke)
// Catch-all gets all non-system events.
assertEventSequence(t, events(r, -1, key.Filter{}), ke)
r = new(Router)
r.Event(key.Filter{Name: "A"})
r.Queue(SystemEvent{ke})
if _, handled := r.WakeupTime(); !handled {
t.Errorf("system event was unexpectedly ignored")
}
// Only specific filters match system events.
assertEventSequence(t, events(r, -1, key.Filter{Name: "A"}), ke)
}
func TestInputHint(t *testing.T) {
r := new(Router)
if hint, changed := r.TextInputHint(); hint != key.HintAny || changed {
t.Fatal("unexpected hint")
}
ops := new(op.Ops)
h := new(int)
key.InputHintOp{Tag: h, Hint: key.HintEmail}.Add(ops)
r.Frame(ops)
if hint, changed := r.TextInputHint(); hint != key.HintAny || changed {
t.Fatal("unexpected hint")
}
r.Source().Execute(key.FocusCmd{Tag: h})
if hint, changed := r.TextInputHint(); hint != key.HintEmail || !changed {
t.Fatal("unexpected hint")
}
}
func TestDeferred(t *testing.T) {
r := new(Router)
h := new(int)
f := []event.Filter{
key.FocusFilter{Target: h},
key.Filter{Name: "A"},
}
// Provoke deferring by exhausting events for h.
events(r, -1, f...)
r.Source().Execute(key.FocusCmd{Tag: h})
ke := key.Event{Name: "A"}
r.Queue(ke)
// All events are deferred at this point.
assertEventSequence(t, events(r, -1, f...))
r.Frame(new(op.Ops))
// But delivered after a frame.
assertEventSequence(t, events(r, -1, f...), key.FocusEvent{Focus: true}, ke)
}
func TestInputWakeup(t *testing.T) {
handler := new(int)
var ops op.Ops
// InputOps shouldn't trigger redraws.
event.Op(&ops, handler)
var r Router
// Reset events shouldn't either.
evts := events(&r, -1, key.FocusFilter{Target: new(int)}, key.Filter{Name: "A"})
assertEventSequence(t, evts, key.FocusEvent{Focus: false})
r.Frame(&ops)
if _, wake := r.WakeupTime(); wake {
t.Errorf("InputOp or the resetting FocusEvent triggered a wakeup")
}
// And neither does events that don't match anything.
r.Queue(key.SnippetEvent{})
if _, handled := r.WakeupTime(); handled {
t.Errorf("a not-matching event triggered a wakeup")
}
// However, events that does match should trigger wakeup.
r.Queue(key.Event{Name: "A"})
if _, handled := r.WakeupTime(); !handled {
t.Errorf("a key.Event didn't trigger redraw")
}
}
func TestKeyMultiples(t *testing.T) {
handlers := make([]int, 3)
r := new(Router)
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
for i := range handlers {
assertEventSequence(t, events(r, 1, key.FocusFilter{Target: &handlers[i]}), key.FocusEvent{Focus: false})
}
r.Source().Execute(key.FocusCmd{Tag: &handlers[2]})
assertEventSequence(t, events(r, -1, key.FocusFilter{Target: &handlers[2]}), key.FocusEvent{Focus: true})
assertFocus(t, r, &handlers[2])
assertKeyboard(t, r, TextInputOpen)
}
func TestKeySoftKeyboardNoFocus(t *testing.T) {
r := new(Router)
// It's possible to open the keyboard
// without any active focus:
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputOpen)
}
func TestKeyRemoveFocus(t *testing.T) {
handlers := make([]int, 2)
r := new(Router)
filters := func(h event.Tag) []event.Filter {
return []event.Filter{
key.FocusFilter{Target: h},
key.Filter{Focus: h, Name: key.NameTab, Required: key.ModShortcut},
}
}
var all []event.Filter
for i := range handlers {
all = append(all, filters(&handlers[i])...)
}
assertEventSequence(t, events(r, len(handlers), all...), key.FocusEvent{}, key.FocusEvent{})
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
evt := key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press}
r.Queue(evt)
assertEventSequence(t, events(r, 2, filters(&handlers[0])...), key.FocusEvent{Focus: true}, evt)
assertFocus(t, r, &handlers[0])
assertKeyboard(t, r, TextInputOpen)
// Frame removes focus from tags that don't filter for focus events nor mentioned in an InputOp.
r.Source().Execute(key.FocusCmd{Tag: new(int)})
r.Frame(new(op.Ops))
assertEventSequence(t, events(r, -1, filters(&handlers[1])...))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose)
// Set focus to InputOp which already
// exists in the previous frame:
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
assertFocus(t, r, &handlers[0])
}
func TestKeyFocusedInvisible(t *testing.T) {
handlers := make([]int, 2)
ops := new(op.Ops)
r := new(Router)
for i := range handlers {
assertEventSequence(t, events(r, 1, key.FocusFilter{Target: &handlers[i]}), key.FocusEvent{Focus: false})
}
// Set new InputOp with focus:
r.Source().Execute(key.FocusCmd{Tag: &handlers[0]})
r.Source().Execute(key.SoftKeyboardCmd{Show: true})
assertEventSequence(t, events(r, 1, key.FocusFilter{Target: &handlers[0]}), key.FocusEvent{Focus: true})
assertFocus(t, r, &handlers[0])
assertKeyboard(t, r, TextInputOpen)
// Frame will clear the focus because the handler is not visible.
r.Frame(ops)
for i := range handlers {
assertEventSequence(t, events(r, -1, key.FocusFilter{Target: &handlers[i]}))
}
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose)
r.Frame(ops)
r.Frame(ops)
ops.Reset()
// Respawn the first element:
// It must receive one `Event{Focus: false}`.
event.Op(ops, &handlers[0])
assertEventSequence(t, events(r, -1, key.FocusFilter{Target: &handlers[0]}), key.FocusEvent{Focus: false})
}
func TestNoOps(t *testing.T) {
r := new(Router)
r.Frame(nil)
}
func TestDirectionalFocus(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
handlers := []image.Rectangle{
image.Rect(10, 10, 50, 50),
image.Rect(50, 20, 100, 80),
image.Rect(20, 26, 60, 80),
image.Rect(10, 60, 50, 100),
}
for i, bounds := range handlers {
cl := clip.Rect(bounds).Push(ops)
event.Op(ops, &handlers[i])
cl.Pop()
events(r, -1, key.FocusFilter{Target: &handlers[i]})
}
r.Frame(ops)
r.MoveFocus(key.FocusLeft)
assertFocus(t, r, &handlers[0])
r.MoveFocus(key.FocusLeft)
assertFocus(t, r, &handlers[0])
r.MoveFocus(key.FocusRight)
assertFocus(t, r, &handlers[1])
r.MoveFocus(key.FocusRight)
assertFocus(t, r, &handlers[1])
r.MoveFocus(key.FocusDown)
assertFocus(t, r, &handlers[2])
r.MoveFocus(key.FocusDown)
assertFocus(t, r, &handlers[2])
r.MoveFocus(key.FocusLeft)
assertFocus(t, r, &handlers[3])
r.MoveFocus(key.FocusUp)
assertFocus(t, r, &handlers[0])
r.MoveFocus(key.FocusForward)
assertFocus(t, r, &handlers[1])
r.MoveFocus(key.FocusBackward)
assertFocus(t, r, &handlers[0])
}
func TestFocusScroll(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
h := new(int)
filters := []event.Filter{
key.FocusFilter{Target: h},
pointer.Filter{
Target: h,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Min: -100, Max: +100},
ScrollY: pointer.ScrollRange{Min: -100, Max: +100},
},
}
events(r, -1, filters...)
parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops)
cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops)
event.Op(ops, h)
// Test that h is scrolled even if behind another handler.
event.Op(ops, new(int))
cl.Pop()
parent.Pop()
r.Frame(ops)
r.MoveFocus(key.FocusLeft)
r.RevealFocus(image.Rect(0, 0, 15, 40))
evts := events(r, -1, filters...)
assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9))
}
func TestFocusClick(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
h := new(int)
filters := []event.Filter{
key.FocusFilter{Target: h},
pointer.Filter{
Target: h,
Kinds: pointer.Press | pointer.Release | pointer.Cancel,
},
}
assertEventPointerTypeSequence(t, events(r, -1, filters...), pointer.Cancel)
cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops)
event.Op(ops, h)
cl.Pop()
r.Frame(ops)
r.MoveFocus(key.FocusLeft)
r.ClickFocus()
assertEventPointerTypeSequence(t, events(r, -1, filters...), pointer.Press, pointer.Release)
}
func TestNoFocus(t *testing.T) {
r := new(Router)
r.MoveFocus(key.FocusForward)
}
func TestKeyRouting(t *testing.T) {
r := new(Router)
h := new(int)
A, B := key.Event{Name: "A"}, key.Event{Name: "B"}
// Register filters.
events(r, -1, key.Filter{Name: "A"}, key.Filter{Name: "B"})
r.Frame(new(op.Ops))
r.Queue(A, B)
// The handler is not focused, so only B is delivered.
assertEventSequence(t, events(r, -1, key.Filter{Focus: h, Name: "A"}, key.Filter{Name: "B"}), B)
r.Source().Execute(key.FocusCmd{Tag: h})
// A is delivered to the focused handler.
assertEventSequence(t, events(r, -1, key.Filter{Focus: h, Name: "A"}, key.Filter{Name: "B"}), A)
}
func assertFocus(t *testing.T, router *Router, expected event.Tag) {
t.Helper()
if !router.Source().Focused(expected) {
t.Errorf("expected %v to be focused", expected)
}
}
func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
t.Helper()
if got := router.state().state; got != expected {
t.Errorf("expected %v keyboard, got %v", expected, got)
}
}
+351 -296
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
package router package input
import ( import (
"image" "image"
@@ -10,7 +10,6 @@ import (
f32internal "gioui.org/internal/f32" f32internal "gioui.org/internal/f32"
"gioui.org/internal/ops" "gioui.org/internal/ops"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system" "gioui.org/io/system"
@@ -18,14 +17,8 @@ import (
) )
type pointerQueue struct { type pointerQueue struct {
hitTree []hitNode hitTree []hitNode
areas []areaNode areas []areaNode
cursor pointer.Cursor
handlers map[event.Tag]*pointerHandler
pointers []pointerInfo
transfers []io.ReadCloser // pending data transfers
scratch []event.Tag
semantic struct { semantic struct {
idsAssigned bool idsAssigned bool
@@ -43,10 +36,15 @@ type hitNode struct {
// For handler nodes. // For handler nodes.
tag event.Tag tag event.Tag
ktag event.Tag
pass bool pass bool
} }
// pointerState is the input state related to pointer events.
type pointerState struct {
cursor pointer.Cursor
pointers []pointerInfo
}
type pointerInfo struct { type pointerInfo struct {
id pointer.ID id pointer.ID
pressed bool pressed bool
@@ -63,17 +61,21 @@ type pointerInfo struct {
} }
type pointerHandler struct { type pointerHandler struct {
area int // areaPlusOne is the index into the list of pointerQueue.areas, plus 1.
active bool areaPlusOne int
wantsGrab bool // setup tracks whether the handler has received
types pointer.Type // the pointer.Cancel event that resets its state.
setup bool
}
// pointerFilter represents the union of a set of pointer filters.
type pointerFilter struct {
kinds pointer.Kind
// min and max horizontal/vertical scroll // min and max horizontal/vertical scroll
scrollRange image.Rectangle scrollX, scrollY pointer.ScrollRange
sourceMimes []string sourceMimes []string
targetMimes []string targetMimes []string
offeredMime string
data io.ReadCloser
} }
type areaOp struct { type areaOp struct {
@@ -229,33 +231,18 @@ func (c *pointerCollector) addHitNode(n hitNode) {
} }
// newHandler returns the current handler or a new one for tag. // newHandler returns the current handler or a new one for tag.
func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *pointerHandler { func (c *pointerCollector) newHandler(tag event.Tag, state *pointerHandler) {
areaID := c.currentArea() areaID := c.currentArea()
c.addHitNode(hitNode{ c.addHitNode(hitNode{
area: areaID, area: areaID,
tag: tag, tag: tag,
pass: c.state.pass > 0, pass: c.state.pass > 0,
}) })
h, ok := c.q.handlers[tag] state.areaPlusOne = areaID + 1
if !ok {
h = new(pointerHandler)
c.q.handlers[tag] = h
// Cancel handlers on (each) first appearance, but don't
// trigger redraw.
events.AddNoRedraw(tag, pointer.Event{Type: pointer.Cancel})
}
h.active = true
h.area = areaID
return h
} }
func (c *pointerCollector) keyInputOp(op key.InputOp) { func (s *pointerHandler) Reset() {
areaID := c.currentArea() s.areaPlusOne = 0
c.addHitNode(hitNode{
area: areaID,
ktag: op.Tag,
pass: true,
})
} }
func (c *pointerCollector) actionInputOp(act system.Action) { func (c *pointerCollector) actionInputOp(act system.Action) {
@@ -264,21 +251,111 @@ func (c *pointerCollector) actionInputOp(act system.Action) {
area.action = act area.action = act
} }
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) { func (q *pointerQueue) grab(state pointerState, req pointer.GrabCmd) (pointerState, []taggedEvent) {
var evts []taggedEvent
for _, p := range state.pointers {
if !p.pressed || p.id != req.ID {
continue
}
// Drop other handlers that lost their grab.
for i := len(p.handlers) - 1; i >= 0; i-- {
if tag := p.handlers[i]; tag != req.Tag {
evts = append(evts, taggedEvent{
tag: tag,
event: pointer.Event{Kind: pointer.Cancel},
})
state = dropHandler(state, tag)
}
}
break
}
return state, evts
}
func (c *pointerCollector) inputOp(tag event.Tag, state *pointerHandler) {
areaID := c.currentArea() areaID := c.currentArea()
area := &c.q.areas[areaID] area := &c.q.areas[areaID]
area.semantic.content.tag = op.Tag area.semantic.content.tag = tag
if op.Types&(pointer.Press|pointer.Release) != 0 { c.newHandler(tag, state)
area.semantic.content.gestures |= ClickGesture }
func (p *pointerFilter) Add(f event.Filter) {
switch f := f.(type) {
case transfer.SourceFilter:
for _, m := range p.sourceMimes {
if m == f.Type {
return
}
}
p.sourceMimes = append(p.sourceMimes, f.Type)
case transfer.TargetFilter:
for _, m := range p.targetMimes {
if m == f.Type {
return
}
}
p.targetMimes = append(p.targetMimes, f.Type)
case pointer.Filter:
p.kinds = p.kinds | f.Kinds
p.scrollX = p.scrollX.Union(f.ScrollX)
p.scrollY = p.scrollY.Union(f.ScrollY)
} }
if op.Types&pointer.Scroll != 0 { }
area.semantic.content.gestures |= ScrollGesture
func (p *pointerFilter) Matches(e event.Event) bool {
switch e := e.(type) {
case pointer.Event:
return e.Kind&p.kinds == e.Kind
case transfer.CancelEvent, transfer.InitiateEvent:
return len(p.sourceMimes) > 0 || len(p.targetMimes) > 0
case transfer.RequestEvent:
for _, t := range p.sourceMimes {
if t == e.Type {
return true
}
}
case transfer.DataEvent:
for _, t := range p.targetMimes {
if t == e.Type {
return true
}
}
} }
area.semantic.valid = area.semantic.content.gestures != 0 return false
h := c.newHandler(op.Tag, events) }
h.wantsGrab = h.wantsGrab || op.Grab
h.types = h.types | op.Types func (p *pointerFilter) Merge(p2 pointerFilter) {
h.scrollRange = op.ScrollBounds p.kinds = p.kinds | p2.kinds
p.scrollX = p.scrollX.Union(p2.scrollX)
p.scrollY = p.scrollY.Union(p2.scrollY)
p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...)
p.targetMimes = append(p.targetMimes, p2.targetMimes...)
}
// clampScroll splits a scroll distance in the remaining scroll and the
// scroll accepted by the filter.
func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) {
left.X, scrolled.X = clampSplit(scroll.X, p.scrollX.Min, p.scrollX.Max)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max)
return
}
func clampSplit(v float32, min, max int) (float32, float32) {
if m := float32(max); v > m {
return v - m, m
}
if m := float32(min); v < m {
return v - m, m
}
return 0, v
}
func (s *pointerHandler) ResetEvent() (event.Event, bool) {
if s.setup {
return nil, false
}
s.setup = true
return pointer.Event{Kind: pointer.Cancel}, true
} }
func (c *pointerCollector) semanticLabel(lbl string) { func (c *pointerCollector) semanticLabel(lbl string) {
@@ -309,11 +386,11 @@ func (c *pointerCollector) semanticSelected(selected bool) {
area.semantic.content.selected = selected area.semantic.content.selected = selected
} }
func (c *pointerCollector) semanticDisabled(disabled bool) { func (c *pointerCollector) semanticEnabled(enabled bool) {
areaID := c.currentArea() areaID := c.currentArea()
area := &c.q.areas[areaID] area := &c.q.areas[areaID]
area.semantic.valid = true area.semantic.valid = true
area.semantic.content.disabled = disabled area.semantic.content.disabled = !enabled
} }
func (c *pointerCollector) cursor(cursor pointer.Cursor) { func (c *pointerCollector) cursor(cursor pointer.Cursor) {
@@ -322,23 +399,28 @@ func (c *pointerCollector) cursor(cursor pointer.Cursor) {
area.cursor = cursor area.cursor = cursor
} }
func (c *pointerCollector) sourceOp(op transfer.SourceOp, events *handlerEvents) { func (q *pointerQueue) offerData(handlers map[event.Tag]*handler, state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) {
h := c.newHandler(op.Tag, events) var evts []taggedEvent
h.sourceMimes = append(h.sourceMimes, op.Type) for i, p := range state.pointers {
if p.dataSource != req.Tag {
continue
}
if p.dataTarget != nil {
evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{
Type: req.Type,
Open: func() io.ReadCloser {
return req.Data
},
}})
}
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[i], evts = q.deliverTransferCancelEvent(handlers, p, evts)
break
}
return state, evts
} }
func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents) { func (c *pointerCollector) Reset() {
h := c.newHandler(op.Tag, events)
h.targetMimes = append(h.targetMimes, op.Type)
}
func (c *pointerCollector) offerOp(op transfer.OfferOp, events *handlerEvents) {
h := c.newHandler(op.Tag, events)
h.offeredMime = op.Type
h.data = op.Data
}
func (c *pointerCollector) reset() {
c.q.reset() c.q.reset()
c.resetState() c.resetState()
c.ensureRoot() c.ensureRoot()
@@ -432,39 +514,42 @@ func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
return id.id return id.id
} }
func (q *pointerQueue) ActionAt(pos f32.Point) (system.Action, bool) { func (q *pointerQueue) ActionAt(pos f32.Point) (action system.Action, hasAction bool) {
for i := len(q.hitTree) - 1; i >= 0; i-- { q.hitTest(pos, func(n *hitNode) bool {
n := &q.hitTree[i]
hit, _ := q.hit(n.area, pos)
if !hit {
continue
}
area := q.areas[n.area] area := q.areas[n.area]
return area.action, area.action != 0 if area.action != 0 {
} action = area.action
return 0, false hasAction = true
return false
}
return true
})
return action, hasAction
} }
func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) { func (q *pointerQueue) SemanticAt(pos f32.Point) (semID SemanticID, hasSemID bool) {
q.assignSemIDs() q.assignSemIDs()
for i := len(q.hitTree) - 1; i >= 0; i-- { q.hitTest(pos, func(n *hitNode) bool {
n := &q.hitTree[i]
hit, _ := q.hit(n.area, pos)
if !hit {
continue
}
area := q.areas[n.area] area := q.areas[n.area]
if area.semantic.id != 0 { if area.semantic.id != 0 {
return area.semantic.id, true semID = area.semantic.id
hasSemID = true
return false
} }
} return true
return 0, false })
return semID, hasSemID
} }
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) { // hitTest searches the hit tree for nodes matching pos. Any node matching pos will
// have the onNode func invoked on it to allow the caller to extract whatever information
// is necessary for further processing. onNode may return false to terminate the walk of
// the hit tree, or true to continue. Providing this algorithm in this generic way
// allows normal event routing and system action event routing to share the same traversal
// logic even though they are interested in different aspects of hit nodes.
func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode) bool) pointer.Cursor {
// Track whether we're passing through hits. // Track whether we're passing through hits.
pass := true pass := true
hits := q.scratch[:0]
idx := len(q.hitTree) - 1 idx := len(q.hitTree) - 1
cursor := pointer.CursorDefault cursor := pointer.CursorDefault
for idx >= 0 { for idx >= 0 {
@@ -483,14 +568,11 @@ func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
} else { } else {
idx = n.next idx = n.next
} }
if n.tag != nil { if !onNode(n) {
if _, exists := q.handlers[n.tag]; exists { break
hits = addHandler(hits, n.tag)
}
} }
} }
q.scratch = hits[:0] return cursor
return hits, cursor
} }
func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point { func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
@@ -517,17 +599,6 @@ func (q *pointerQueue) hit(areaIdx int, p f32.Point) (bool, pointer.Cursor) {
} }
func (q *pointerQueue) reset() { func (q *pointerQueue) reset() {
if q.handlers == nil {
q.handlers = make(map[event.Tag]*pointerHandler)
}
for _, h := range q.handlers {
// Reset handler.
h.active = false
h.wantsGrab = false
h.types = 0
h.sourceMimes = h.sourceMimes[:0]
h.targetMimes = h.targetMimes[:0]
}
q.hitTree = q.hitTree[:0] q.hitTree = q.hitTree[:0]
q.areas = q.areas[:0] q.areas = q.areas[:0]
q.semantic.idsAssigned = false q.semantic.idsAssigned = false
@@ -545,80 +616,71 @@ func (q *pointerQueue) reset() {
delete(q.semantic.contentIDs, k) delete(q.semantic.contentIDs, k)
} }
} }
for _, rc := range q.transfers {
if rc != nil {
rc.Close()
}
}
q.transfers = nil
} }
func (q *pointerQueue) Frame(events *handlerEvents) { func (q *pointerQueue) Frame(handlers map[event.Tag]*handler, state pointerState) (pointerState, []taggedEvent) {
for k, h := range q.handlers { for _, h := range handlers {
if !h.active { if h.pointer.areaPlusOne != 0 {
q.dropHandler(nil, k) area := &q.areas[h.pointer.areaPlusOne-1]
delete(q.handlers, k) if h.filter.pointer.kinds&(pointer.Press|pointer.Release) != 0 {
} area.semantic.content.gestures |= ClickGesture
if h.wantsGrab {
for _, p := range q.pointers {
if !p.pressed {
continue
}
for i, k2 := range p.handlers {
if k2 == k {
// Drop other handlers that lost their grab.
dropped := q.scratch[:0]
dropped = append(dropped, p.handlers[:i]...)
dropped = append(dropped, p.handlers[i+1:]...)
for _, tag := range dropped {
q.dropHandler(events, tag)
}
break
}
}
} }
if h.filter.pointer.kinds&pointer.Scroll != 0 {
area.semantic.content.gestures |= ScrollGesture
}
area.semantic.valid = area.semantic.content.gestures != 0
} }
} }
for i := range q.pointers { var evts []taggedEvent
p := &q.pointers[i] for i, p := range state.pointers {
q.deliverEnterLeaveEvents(p, events, p.last) changed := false
q.deliverTransferDataEvent(p, events) p, evts, state.cursor, changed = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, p.last)
if changed {
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[i] = p
}
} }
return state, evts
} }
func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) { func dropHandler(state pointerState, tag event.Tag) pointerState {
if events != nil { pointers := state.pointers
events.Add(tag, pointer.Event{Type: pointer.Cancel}) state.pointers = nil
} for _, p := range pointers {
for i := range q.pointers { handlers := p.handlers
p := &q.pointers[i] p.handlers = nil
for i := len(p.handlers) - 1; i >= 0; i-- { for _, h := range handlers {
if p.handlers[i] == tag { if h != tag {
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...) p.handlers = append(p.handlers, h)
} }
} }
for i := len(p.entered) - 1; i >= 0; i-- { entered := p.entered
if p.entered[i] == tag { p.entered = nil
p.entered = append(p.entered[:i], p.entered[i+1:]...) for _, h := range entered {
if h != tag {
p.entered = append(p.entered, h)
} }
} }
state.pointers = append(state.pointers, p)
} }
return state
} }
// pointerOf returns the pointerInfo index corresponding to the pointer in e. // pointerOf returns the pointerInfo index corresponding to the pointer in e.
func (q *pointerQueue) pointerOf(e pointer.Event) int { func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) {
for i, p := range q.pointers { for i, p := range s.pointers {
if p.id == e.PointerID { if p.id == e.PointerID {
return i return s, i
} }
} }
q.pointers = append(q.pointers, pointerInfo{id: e.PointerID}) n := len(s.pointers)
return len(q.pointers) - 1 s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID})
return s, len(s.pointers) - 1
} }
// Deliver is like Push, but delivers an event to a particular area. // Deliver is like Push, but delivers an event to a particular area.
func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEvents) { func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e pointer.Event) []taggedEvent {
var sx, sy = e.Scroll.X, e.Scroll.Y scroll := e.Scroll
idx := len(q.hitTree) - 1 idx := len(q.hitTree) - 1
// Locate first potential receiver. // Locate first potential receiver.
for idx != -1 { for idx != -1 {
@@ -628,31 +690,28 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
} }
idx-- idx--
} }
var evts []taggedEvent
for idx != -1 { for idx != -1 {
n := &q.hitTree[idx] n := &q.hitTree[idx]
idx = n.next idx = n.next
if n.tag == nil { h, ok := handlers[n.tag]
continue if !ok || !h.filter.pointer.Matches(e) {
}
h := q.handlers[n.tag]
if e.Type&h.types == 0 {
continue continue
} }
e := e e := e
if e.Type == pointer.Scroll { if e.Kind == pointer.Scroll {
if sx == 0 && sy == 0 { if scroll == (f32.Point{}) {
break break
} }
// Distribute the scroll to the handler based on its ScrollRange. scroll, e.Scroll = h.filter.pointer.clampScroll(scroll)
sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, h.scrollRange.Max.X)
sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, h.scrollRange.Max.Y)
} }
e.Position = q.invTransform(h.area, e.Position) e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
events.Add(n.tag, e) evts = append(evts, taggedEvent{tag: n.tag, event: e})
if e.Type != pointer.Scroll { if e.Kind != pointer.Scroll {
break break
} }
} }
return evts
} }
// SemanticArea returns the sematic content for area, and its parent area. // SemanticArea returns the sematic content for area, and its parent area.
@@ -668,106 +727,129 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
return semanticContent{}, -1 return semanticContent{}, -1
} }
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) { func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState, e pointer.Event) (pointerState, []taggedEvent) {
if e.Type == pointer.Cancel { var evts []taggedEvent
q.pointers = q.pointers[:0] if e.Kind == pointer.Cancel {
for k := range q.handlers { for k := range handlers {
q.dropHandler(events, k) evts = append(evts, taggedEvent{
event: pointer.Event{Kind: pointer.Cancel},
tag: k,
})
} }
return state.pointers = nil
return state, evts
} }
pidx := q.pointerOf(e) state, pidx := state.pointerOf(e)
p := &q.pointers[pidx] p := state.pointers[pidx]
p.last = e
switch e.Type { switch e.Kind {
case pointer.Press: case pointer.Press:
q.deliverEnterLeaveEvents(p, events, e) p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
p.pressed = true p.pressed = true
q.deliverEvent(p, events, e) evts = q.deliverEvent(handlers, p, evts, e)
case pointer.Move: case pointer.Move:
if p.pressed { if p.pressed {
e.Type = pointer.Drag e.Kind = pointer.Drag
} }
q.deliverEnterLeaveEvents(p, events, e) p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
q.deliverEvent(p, events, e) evts = q.deliverEvent(handlers, p, evts, e)
if p.pressed { if p.pressed {
q.deliverDragEvent(p, events) p, evts = q.deliverDragEvent(handlers, p, evts)
} }
case pointer.Release: case pointer.Release:
q.deliverEvent(p, events, e) evts = q.deliverEvent(handlers, p, evts, e)
p.pressed = false p.pressed = false
q.deliverEnterLeaveEvents(p, events, e) p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
q.deliverDropEvent(p, events) p, evts = q.deliverDropEvent(handlers, p, evts)
case pointer.Scroll: case pointer.Scroll:
q.deliverEnterLeaveEvents(p, events, e) p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
q.deliverEvent(p, events, e) evts = q.deliverEvent(handlers, p, evts, e)
default: default:
panic("unsupported pointer event type") panic("unsupported pointer event type")
} }
p.last = e
if !p.pressed && len(p.entered) == 0 { if !p.pressed && len(p.entered) == 0 {
// No longer need to track pointer. // No longer need to track pointer.
q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...) state.pointers = append(state.pointers[:pidx:pidx], state.pointers[pidx+1:]...)
} else {
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[pidx] = p
} }
return state, evts
} }
func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) { func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent {
foremost := true foremost := true
if p.pressed && len(p.handlers) == 1 { if p.pressed && len(p.handlers) == 1 {
e.Priority = pointer.Grabbed e.Priority = pointer.Grabbed
foremost = false foremost = false
} }
var sx, sy = e.Scroll.X, e.Scroll.Y scroll := e.Scroll
for _, k := range p.handlers { for _, k := range p.handlers {
h := q.handlers[k] h, ok := handlers[k]
if e.Type == pointer.Scroll { if !ok {
if sx == 0 && sy == 0 {
return
}
// Distribute the scroll to the handler based on its ScrollRange.
sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, h.scrollRange.Max.X)
sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, h.scrollRange.Max.Y)
}
if e.Type&h.types == 0 {
continue continue
} }
f := h.filter.pointer
if !f.Matches(e) {
continue
}
if e.Kind == pointer.Scroll {
if scroll == (f32.Point{}) {
return evts
}
scroll, e.Scroll = f.clampScroll(scroll)
}
e := e e := e
if foremost { if foremost {
foremost = false foremost = false
e.Priority = pointer.Foremost e.Priority = pointer.Foremost
} }
e.Position = q.invTransform(h.area, e.Position) e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
events.Add(k, e) evts = append(evts, taggedEvent{event: e, tag: k})
} }
return evts
} }
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) { func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler, cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) {
changed := false
var hits []event.Tag var hits []event.Tag
if e.Source != pointer.Mouse && !p.pressed && e.Type != pointer.Press { if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press {
// Consider non-mouse pointers leaving when they're released. // Consider non-mouse pointers leaving when they're released.
} else { } else {
hits, q.cursor = q.opHit(e.Position) var transSrc *pointerFilter
if p.pressed { if p.dataSource != nil {
// Filter out non-participating handlers, transSrc = &handlers[p.dataSource].filter.pointer
// except potential transfer targets when a transfer has been initiated. }
var hitsHaveTarget bool cursor = q.hitTest(e.Position, func(n *hitNode) bool {
if p.dataSource != nil { h, ok := handlers[n.tag]
transferSource := q.handlers[p.dataSource] if !ok {
for _, hit := range hits { return true
if _, ok := firstMimeMatch(transferSource, q.handlers[hit]); ok { }
hitsHaveTarget = true add := true
break if p.pressed {
add = false
// Filter out non-participating handlers,
// except potential transfer targets when a transfer has been initiated.
if _, found := searchTag(p.handlers, n.tag); found {
add = true
}
if transSrc != nil {
if _, ok := firstMimeMatch(transSrc, &h.filter.pointer); ok {
add = true
} }
} }
} }
for i := len(hits) - 1; i >= 0; i-- { if add {
if _, found := searchTag(p.handlers, hits[i]); !found && !hitsHaveTarget { hits = addHandler(hits, n.tag)
hits = append(hits[:i], hits[i+1:]...)
}
} }
} else { return true
p.handlers = append(p.handlers[:0], hits...) })
if !p.pressed {
changed = true
p.handlers = hits
} }
} }
// Deliver Leave events. // Deliver Leave events.
@@ -775,111 +857,94 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
if _, found := searchTag(hits, k); found { if _, found := searchTag(hits, k); found {
continue continue
} }
h := q.handlers[k] h, ok := handlers[k]
e.Type = pointer.Leave if !ok {
continue
}
changed = true
e := e
e.Kind = pointer.Leave
if e.Type&h.types != 0 { if h.filter.pointer.Matches(e) {
e := e e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
e.Position = q.invTransform(h.area, e.Position) evts = append(evts, taggedEvent{tag: k, event: e})
events.Add(k, e)
} }
} }
// Deliver Enter events. // Deliver Enter events.
for _, k := range hits { for _, k := range hits {
h := q.handlers[k]
if _, found := searchTag(p.entered, k); found { if _, found := searchTag(p.entered, k); found {
continue continue
} }
e.Type = pointer.Enter h, ok := handlers[k]
if !ok {
continue
}
changed = true
e := e
e.Kind = pointer.Enter
if e.Type&h.types != 0 { if h.filter.pointer.Matches(e) {
e := e e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
e.Position = q.invTransform(h.area, e.Position) evts = append(evts, taggedEvent{tag: k, event: e})
events.Add(k, e)
} }
} }
p.entered = append(p.entered[:0], hits...) p.entered = hits
return p, evts, cursor, changed
} }
func (q *pointerQueue) deliverDragEvent(p *pointerInfo, events *handlerEvents) { func (q *pointerQueue) deliverDragEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
if p.dataSource != nil { if p.dataSource != nil {
return return p, evts
} }
// Identify the data source. // Identify the data source.
for _, k := range p.entered { for _, k := range p.entered {
src := q.handlers[k] src := &handlers[k].filter.pointer
if len(src.sourceMimes) == 0 { if len(src.sourceMimes) == 0 {
continue continue
} }
// One data source handler per pointer. // One data source handler per pointer.
p.dataSource = k p.dataSource = k
// Notify all potential targets. // Notify all potential targets.
for k, tgt := range q.handlers { for k, tgt := range handlers {
if _, ok := firstMimeMatch(src, tgt); ok { if _, ok := firstMimeMatch(src, &tgt.filter.pointer); ok {
events.Add(k, transfer.InitiateEvent{}) evts = append(evts, taggedEvent{tag: k, event: transfer.InitiateEvent{}})
} }
} }
break break
} }
return p, evts
} }
func (q *pointerQueue) deliverDropEvent(p *pointerInfo, events *handlerEvents) { func (q *pointerQueue) deliverDropEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
if p.dataSource == nil { if p.dataSource == nil {
return return p, evts
} }
// Request data from the source. // Request data from the source.
src := q.handlers[p.dataSource] src := &handlers[p.dataSource].filter.pointer
for _, k := range p.entered { for _, k := range p.entered {
h := q.handlers[k] h := handlers[k]
if m, ok := firstMimeMatch(src, h); ok { if m, ok := firstMimeMatch(src, &h.filter.pointer); ok {
p.dataTarget = k p.dataTarget = k
events.Add(p.dataSource, transfer.RequestEvent{Type: m}) evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.RequestEvent{Type: m}})
return return p, evts
} }
} }
// No valid target found, abort. // No valid target found, abort.
q.deliverTransferCancelEvent(p, events) return q.deliverTransferCancelEvent(handlers, p, evts)
} }
func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerEvents) { func (q *pointerQueue) deliverTransferCancelEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
if p.dataSource == nil { evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.CancelEvent{}})
return
}
src := q.handlers[p.dataSource]
if src.data == nil {
// Data not received yet.
return
}
if p.dataTarget == nil {
q.deliverTransferCancelEvent(p, events)
return
}
// Send the offered data to the target.
transferIdx := len(q.transfers)
events.Add(p.dataTarget, transfer.DataEvent{
Type: src.offeredMime,
Open: func() io.ReadCloser {
q.transfers[transferIdx] = nil
return src.data
},
})
q.transfers = append(q.transfers, src.data)
p.dataTarget = nil
}
func (q *pointerQueue) deliverTransferCancelEvent(p *pointerInfo, events *handlerEvents) {
events.Add(p.dataSource, transfer.CancelEvent{})
// Cancel all potential targets. // Cancel all potential targets.
src := q.handlers[p.dataSource] src := &handlers[p.dataSource].filter.pointer
for k, h := range q.handlers { for k, h := range handlers {
if _, ok := firstMimeMatch(src, h); ok { if _, ok := firstMimeMatch(src, &h.filter.pointer); ok {
events.Add(k, transfer.CancelEvent{}) evts = append(evts, taggedEvent{tag: k, event: transfer.CancelEvent{}})
} }
} }
src.offeredMime = ""
src.data = nil
p.dataSource = nil p.dataSource = nil
p.dataTarget = nil p.dataTarget = nil
return p, evts
} }
// ClipFor clips r to the parents of area. // ClipFor clips r to the parents of area.
@@ -914,7 +979,7 @@ func addHandler(tags []event.Tag, tag event.Tag) []event.Tag {
} }
// firstMimeMatch returns the first type match between src and tgt. // firstMimeMatch returns the first type match between src and tgt.
func firstMimeMatch(src, tgt *pointerHandler) (first string, matched bool) { func firstMimeMatch(src, tgt *pointerFilter) (first string, matched bool) {
for _, m1 := range tgt.targetMimes { for _, m1 := range tgt.targetMimes {
for _, m2 := range src.sourceMimes { for _, m2 := range src.sourceMimes {
if m1 == m2 { if m1 == m2 {
@@ -951,13 +1016,3 @@ func (a *areaNode) bounds() image.Rectangle {
Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)), Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)),
}.Round() }.Round()
} }
func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) {
if v := float32(max); scroll > v {
return scroll - v, v
}
if v := float32(min); scroll < v {
return scroll - v, v
}
return 0, scroll
}
File diff suppressed because it is too large Load Diff
+881
View File
@@ -0,0 +1,881 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"image"
"io"
"strings"
"time"
"gioui.org/f32"
f32internal "gioui.org/internal/f32"
"gioui.org/internal/ops"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
)
// Router tracks the [io/event.Tag] identifiers of user interface widgets
// and routes events to them. [Source] is its interface exposed to widgets.
type Router struct {
savedTrans []f32.Affine2D
transStack []f32.Affine2D
handlers map[event.Tag]*handler
pointer struct {
queue pointerQueue
collector pointerCollector
}
key struct {
queue keyQueue
// The following fields have the same purpose as the fields in
// type handler, but for key.Events.
filter keyFilter
nextFilter keyFilter
scratchFilter keyFilter
}
cqueue clipboardQueue
// states is the list of pending state changes resulting from
// incoming events. The first element, if present, contains the state
// and events for the current frame.
changes []stateChange
reader ops.Reader
// InvalidateCmd summary.
wakeup bool
wakeupTime time.Time
// Changes queued for next call to Frame.
commands []Command
// transfers is the pending transfer.DataEvent.Open functions.
transfers []io.ReadCloser
// deferring is set if command execution and event delivery is deferred
// to the next frame.
deferring bool
// scratchFilters is for garbage-free construction of ephemeral filters.
scratchFilters []taggedFilter
}
// Source implements the interface between a Router and user interface widgets.
// The value Source is disabled.
type Source struct {
r *Router
}
// Command represents a request such as moving the focus, or initiating a clipboard read.
// Commands are queued by calling [Source.Queue].
type Command interface {
ImplementsCommand()
}
// SemanticNode represents a node in the tree describing the components
// contained in a frame.
type SemanticNode struct {
ID SemanticID
ParentID SemanticID
Children []SemanticNode
Desc SemanticDesc
areaIdx int
}
// SemanticDesc provides a semantic description of a UI component.
type SemanticDesc struct {
Class semantic.ClassOp
Description string
Label string
Selected bool
Disabled bool
Gestures SemanticGestures
Bounds image.Rectangle
}
// SemanticGestures is a bit-set of supported gestures.
type SemanticGestures int
const (
ClickGesture SemanticGestures = 1 << iota
ScrollGesture
)
// SemanticID uniquely identifies a SemanticDescription.
//
// By convention, the zero value denotes the non-existent ID.
type SemanticID uint
// SystemEvent is a marker for events that have platform specific
// side-effects. SystemEvents are never matched by catch-all filters.
type SystemEvent struct {
Event event.Event
}
// handler contains the per-handler state tracked by a [Router].
type handler struct {
// active tracks whether the handler was active in the current
// frame. Router deletes state belonging to inactive handlers during Frame.
active bool
pointer pointerHandler
key keyHandler
// filter the handler has asked for through event handling
// in the previous frame. It is used for routing events in the
// current frame.
filter filter
// prevFilter is the filter being built in the current frame.
nextFilter filter
// processedFilter is the filters that have exhausted available events.
processedFilter filter
}
// filter is the union of a set of [io/event.Filters].
type filter struct {
pointer pointerFilter
focusable bool
}
// taggedFilter is a filter for a particular tag.
type taggedFilter struct {
tag event.Tag
filter filter
}
// stateChange represents the new state and outgoing events
// resulting from an incoming event.
type stateChange struct {
// event, if set, is the trigger for the change.
event event.Event
state inputState
events []taggedEvent
}
// inputState represent a immutable snapshot of the state required
// to route events.
type inputState struct {
clipboardState
keyState
pointerState
}
// taggedEvent represents an event and its target handler.
type taggedEvent struct {
event event.Event
tag event.Tag
}
// Source returns a Source backed by this Router.
func (q *Router) Source() Source {
return Source{r: q}
}
// Execute a command.
func (s Source) Execute(c Command) {
if !s.Enabled() {
return
}
s.r.execute(c)
}
// Enabled reports whether the source is enabled. Only enabled
// Sources deliver events and respond to commands.
func (s Source) Enabled() bool {
return s.r != nil
}
// Focused reports whether tag is focused, according to the most recent
// [key.FocusEvent] delivered.
func (s Source) Focused(tag event.Tag) bool {
if !s.Enabled() {
return false
}
return s.r.state().keyState.focus == tag
}
// Event returns the next event that matches at least one of filters.
func (s Source) Event(filters ...event.Filter) (event.Event, bool) {
if !s.Enabled() {
return nil, false
}
return s.r.Event(filters...)
}
func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
// Merge filters into scratch filters.
q.scratchFilters = q.scratchFilters[:0]
q.key.scratchFilter = q.key.scratchFilter[:0]
for _, f := range filters {
var t event.Tag
switch f := f.(type) {
case key.Filter:
q.key.scratchFilter = append(q.key.scratchFilter, f)
continue
case transfer.SourceFilter:
t = f.Target
case transfer.TargetFilter:
t = f.Target
case key.FocusFilter:
t = f.Target
case pointer.Filter:
t = f.Target
}
if t == nil {
continue
}
var filter *filter
for i := range q.scratchFilters {
s := &q.scratchFilters[i]
if s.tag == t {
filter = &s.filter
break
}
}
if filter == nil {
n := len(q.scratchFilters)
if n < cap(q.scratchFilters) {
// Re-use previously allocated filter.
q.scratchFilters = q.scratchFilters[:n+1]
tf := &q.scratchFilters[n]
tf.tag = t
filter = &tf.filter
filter.Reset()
} else {
q.scratchFilters = append(q.scratchFilters, taggedFilter{tag: t})
filter = &q.scratchFilters[n].filter
}
}
filter.Add(f)
}
for _, tf := range q.scratchFilters {
h := q.stateFor(tf.tag)
h.filter.Merge(tf.filter)
h.nextFilter.Merge(tf.filter)
}
q.key.filter = append(q.key.filter, q.key.scratchFilter...)
q.key.nextFilter = append(q.key.nextFilter, q.key.scratchFilter...)
// Deliver reset event, if any.
for _, f := range filters {
switch f := f.(type) {
case key.FocusFilter:
if f.Target == nil {
break
}
h := q.stateFor(f.Target)
if reset, ok := h.key.ResetEvent(); ok {
return reset, true
}
case pointer.Filter:
if f.Target == nil {
break
}
h := q.stateFor(f.Target)
if reset, ok := h.pointer.ResetEvent(); ok && h.filter.pointer.Matches(reset) {
return reset, true
}
}
}
for i := range q.changes {
if q.deferring && i > 0 {
break
}
change := &q.changes[i]
for j, evt := range change.events {
match := false
switch e := evt.event.(type) {
case key.Event:
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
default:
for _, tf := range q.scratchFilters {
if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
match = true
break
}
}
}
if match {
change.events = append(change.events[:j], change.events[j+1:]...)
// Fast forward state to last matched.
q.collapseState(i)
return evt.event, true
}
}
}
for _, tf := range q.scratchFilters {
h := q.stateFor(tf.tag)
h.processedFilter.Merge(tf.filter)
}
return nil, false
}
// collapseState in the interval [1;idx] into q.changes[0].
func (q *Router) collapseState(idx int) {
if idx == 0 {
return
}
first := &q.changes[0]
first.state = q.changes[idx].state
for _, ch := range q.changes[1 : idx+1] {
first.events = append(first.events, ch.events...)
}
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
}
// Frame completes the current frame and starts a new with the
// handlers from the frame argument. Remaining events are discarded,
// unless they were deferred by a command.
func (q *Router) Frame(frame *op.Ops) {
var remaining []event.Event
if n := len(q.changes); n > 0 {
if q.deferring {
// Collect events for replay.
for _, ch := range q.changes[1:] {
remaining = append(remaining, ch.event)
}
q.changes = append(q.changes[:0], stateChange{state: q.changes[0].state})
} else {
// Collapse state.
state := q.changes[n-1].state
q.changes = append(q.changes[:0], stateChange{state: state})
}
}
for _, rc := range q.transfers {
if rc != nil {
rc.Close()
}
}
q.transfers = nil
q.deferring = false
for _, h := range q.handlers {
h.filter, h.nextFilter = h.nextFilter, h.filter
h.nextFilter.Reset()
h.processedFilter.Reset()
h.pointer.Reset()
h.key.Reset()
}
q.key.filter, q.key.nextFilter = q.key.nextFilter, q.key.filter
q.key.nextFilter = q.key.nextFilter[:0]
var ops *ops.Ops
if frame != nil {
ops = &frame.Internal
}
q.reader.Reset(ops)
q.collect()
for k, h := range q.handlers {
if !h.active {
delete(q.handlers, k)
} else {
h.active = false
}
}
q.executeCommands()
q.Queue(remaining...)
st := q.lastState()
pst, evts := q.pointer.queue.Frame(q.handlers, st.pointerState)
st.pointerState = pst
st.keyState = q.key.queue.Frame(q.handlers, q.lastState().keyState)
q.changeState(nil, st, evts)
// Collapse state and events.
q.collapseState(len(q.changes) - 1)
}
// Queue events to be routed.
func (q *Router) Queue(events ...event.Event) {
for _, e := range events {
se, system := e.(SystemEvent)
if system {
e = se.Event
}
q.processEvent(e, system)
}
}
func (f *filter) Add(flt event.Filter) {
switch flt := flt.(type) {
case key.FocusFilter:
f.focusable = true
case pointer.Filter:
f.pointer.Add(flt)
case transfer.SourceFilter, transfer.TargetFilter:
f.pointer.Add(flt)
}
}
// Merge f2 into f.
func (f *filter) Merge(f2 filter) {
f.focusable = f.focusable || f2.focusable
f.pointer.Merge(f2.pointer)
}
func (f *filter) Matches(e event.Event) bool {
switch e.(type) {
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent:
return f.focusable
default:
return f.pointer.Matches(e)
}
}
func (f *filter) Reset() {
*f = filter{
pointer: pointerFilter{
sourceMimes: f.pointer.sourceMimes[:0],
targetMimes: f.pointer.targetMimes[:0],
},
}
}
func (q *Router) processEvent(e event.Event, system bool) {
state := q.lastState()
switch e := e.(type) {
case pointer.Event:
pstate, evts := q.pointer.queue.Push(q.handlers, state.pointerState, e)
state.pointerState = pstate
q.changeState(e, state, evts)
case key.Event:
var evts []taggedEvent
if q.key.filter.Matches(state.keyState.focus, e, system) {
evts = append(evts, taggedEvent{event: e})
}
q.changeState(e, state, evts)
case key.SnippetEvent:
// Expand existing, overlapping snippet.
if r := state.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
if e.Start > r.Start {
e.Start = r.Start
}
if e.End < r.End {
e.End = r.End
}
}
var evts []taggedEvent
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
q.changeState(e, state, evts)
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
var evts []taggedEvent
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
q.changeState(e, state, evts)
case transfer.DataEvent:
cstate, evts := q.cqueue.Push(state.clipboardState, e)
state.clipboardState = cstate
q.changeState(e, state, evts)
default:
panic("unknown event type")
}
}
func (q *Router) execute(c Command) {
// The command can be executed immediately if event delivery is not frozen, and
// no event receiver has completed their event handling.
if !q.deferring {
ch := q.executeCommand(c)
immediate := true
for _, e := range ch.events {
h, ok := q.handlers[e.tag]
immediate = immediate && (!ok || !h.processedFilter.Matches(e.event))
}
if immediate {
// Hold on to the remaining events for state replay.
var evts []event.Event
for _, ch := range q.changes {
if ch.event != nil {
evts = append(evts, ch.event)
}
}
if len(q.changes) > 1 {
q.changes = q.changes[:1]
}
q.changeState(nil, ch.state, ch.events)
q.Queue(evts...)
return
}
}
q.deferring = true
q.commands = append(q.commands, c)
}
func (q *Router) state() inputState {
if len(q.changes) > 0 {
return q.changes[0].state
}
return inputState{}
}
func (q *Router) lastState() inputState {
if n := len(q.changes); n > 0 {
return q.changes[n-1].state
}
return inputState{}
}
func (q *Router) executeCommands() {
for _, c := range q.commands {
ch := q.executeCommand(c)
q.changeState(nil, ch.state, ch.events)
}
q.commands = nil
}
// executeCommand the command and return the resulting state change along with the
// tag the state change depended on, if any.
func (q *Router) executeCommand(c Command) stateChange {
state := q.state()
var evts []taggedEvent
switch req := c.(type) {
case key.SelectionCmd:
state.keyState = q.key.queue.setSelection(state.keyState, req)
case key.FocusCmd:
state.keyState, evts = q.key.queue.Focus(q.handlers, state.keyState, req.Tag)
case key.SoftKeyboardCmd:
state.keyState = state.keyState.softKeyboard(req.Show)
case key.SnippetCmd:
state.keyState = q.key.queue.setSnippet(state.keyState, req)
case transfer.OfferCmd:
state.pointerState, evts = q.pointer.queue.offerData(q.handlers, state.pointerState, req)
case clipboard.WriteCmd:
q.cqueue.ProcessWriteClipboard(req)
case clipboard.ReadCmd:
state.clipboardState = q.cqueue.ProcessReadClipboard(state.clipboardState, req.Tag)
case pointer.GrabCmd:
state.pointerState, evts = q.pointer.queue.grab(state.pointerState, req)
case op.InvalidateCmd:
if !q.wakeup || req.At.Before(q.wakeupTime) {
q.wakeup = true
q.wakeupTime = req.At
}
}
return stateChange{state: state, events: evts}
}
func (q *Router) changeState(e event.Event, state inputState, evts []taggedEvent) {
// Wrap pointer.DataEvent.Open functions to detect them not being called.
for i := range evts {
e := &evts[i]
if de, ok := e.event.(transfer.DataEvent); ok {
transferIdx := len(q.transfers)
data := de.Open()
q.transfers = append(q.transfers, data)
de.Open = func() io.ReadCloser {
q.transfers[transferIdx] = nil
return data
}
e.event = de
}
}
// Initialize the first change to contain the current state
// and events that are bound for the current frame.
if len(q.changes) == 0 {
q.changes = append(q.changes, stateChange{})
}
if e != nil && len(evts) > 0 {
// An event triggered events bound for user receivers. Add a state change to be
// able to redo the change in case of a command execution.
q.changes = append(q.changes, stateChange{event: e, state: state, events: evts})
} else {
// Otherwise, merge with previous change.
prev := &q.changes[len(q.changes)-1]
prev.state = state
prev.events = append(prev.events, evts...)
}
}
func rangeOverlaps(r1, r2 key.Range) bool {
r1 = rangeNorm(r1)
r2 = rangeNorm(r2)
return r1.Start <= r2.Start && r2.Start < r1.End ||
r1.Start <= r2.End && r2.End < r1.End
}
func rangeNorm(r key.Range) key.Range {
if r.End < r.Start {
r.End, r.Start = r.Start, r.End
}
return r
}
func (q *Router) MoveFocus(dir key.FocusDirection) {
state := q.lastState()
kstate, evts := q.key.queue.MoveFocus(q.handlers, state.keyState, dir)
state.keyState = kstate
q.changeState(nil, state, evts)
}
// RevealFocus scrolls the current focus (if any) into viewport
// if there are scrollable parent handlers.
func (q *Router) RevealFocus(viewport image.Rectangle) {
state := q.lastState()
focus := state.focus
if focus == nil {
return
}
kh := &q.handlers[focus].key
bounds := q.key.queue.BoundsFor(kh)
area := q.key.queue.AreaFor(kh)
viewport = q.pointer.queue.ClipFor(area, viewport)
topleft := bounds.Min.Sub(viewport.Min)
topleft = max(topleft, bounds.Max.Sub(viewport.Max))
topleft = min(image.Pt(0, 0), topleft)
bottomright := bounds.Max.Sub(viewport.Max)
bottomright = min(bottomright, bounds.Min.Sub(viewport.Min))
bottomright = max(image.Pt(0, 0), bottomright)
s := topleft
if s.X == 0 {
s.X = bottomright.X
}
if s.Y == 0 {
s.Y = bottomright.Y
}
q.ScrollFocus(s)
}
// ScrollFocus scrolls the focused widget, if any, by dist.
func (q *Router) ScrollFocus(dist image.Point) {
state := q.lastState()
focus := state.focus
if focus == nil {
return
}
kh := &q.handlers[focus].key
area := q.key.queue.AreaFor(kh)
q.changeState(nil, q.lastState(), q.pointer.queue.Deliver(q.handlers, area, pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Touch,
Scroll: f32internal.FPt(dist),
}))
}
func max(p1, p2 image.Point) image.Point {
m := p1
if p2.X > m.X {
m.X = p2.X
}
if p2.Y > m.Y {
m.Y = p2.Y
}
return m
}
func min(p1, p2 image.Point) image.Point {
m := p1
if p2.X < m.X {
m.X = p2.X
}
if p2.Y < m.Y {
m.Y = p2.Y
}
return m
}
func (q *Router) ActionAt(p f32.Point) (system.Action, bool) {
return q.pointer.queue.ActionAt(p)
}
func (q *Router) ClickFocus() {
focus := q.lastState().focus
if focus == nil {
return
}
kh := &q.handlers[focus].key
bounds := q.key.queue.BoundsFor(kh)
center := bounds.Max.Add(bounds.Min).Div(2)
e := pointer.Event{
Position: f32.Pt(float32(center.X), float32(center.Y)),
Source: pointer.Touch,
}
area := q.key.queue.AreaFor(kh)
e.Kind = pointer.Press
state := q.lastState()
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
e.Kind = pointer.Release
q.changeState(nil, state, q.pointer.queue.Deliver(q.handlers, area, e))
}
// TextInputState returns the input state from the most recent
// call to Frame.
func (q *Router) TextInputState() TextInputState {
state := q.state()
kstate, s := state.InputState()
state.keyState = kstate
q.changeState(nil, state, nil)
return s
}
// TextInputHint returns the input mode from the most recent key.InputOp.
func (q *Router) TextInputHint() (key.InputHint, bool) {
return q.key.queue.InputHint(q.handlers, q.state().keyState)
}
// WriteClipboard returns the most recent content to be copied
// to the clipboard, if any.
func (q *Router) WriteClipboard() (mime string, content []byte, ok bool) {
return q.cqueue.WriteClipboard()
}
// ClipboardRequested reports if any new handler is waiting
// to read the clipboard.
func (q *Router) ClipboardRequested() bool {
return q.cqueue.ClipboardRequested(q.lastState().clipboardState)
}
// Cursor returns the last cursor set.
func (q *Router) Cursor() pointer.Cursor {
return q.state().cursor
}
// SemanticAt returns the first semantic description under pos, if any.
func (q *Router) SemanticAt(pos f32.Point) (SemanticID, bool) {
return q.pointer.queue.SemanticAt(pos)
}
// AppendSemantics appends the semantic tree to nodes, and returns the result.
// The root node is the first added.
func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
q.pointer.collector.q = &q.pointer.queue
q.pointer.collector.ensureRoot()
return q.pointer.queue.AppendSemantics(nodes)
}
// EditorState returns the editor state for the focused handler, or the
// zero value if there is none.
func (q *Router) EditorState() EditorState {
return q.key.queue.editorState(q.handlers, q.state().keyState)
}
func (q *Router) stateFor(tag event.Tag) *handler {
if tag == nil {
panic("internal error: nil tag")
}
s, ok := q.handlers[tag]
if !ok {
s = new(handler)
if q.handlers == nil {
q.handlers = make(map[event.Tag]*handler)
}
q.handlers[tag] = s
}
s.active = true
return s
}
func (q *Router) collect() {
q.transStack = q.transStack[:0]
pc := &q.pointer.collector
pc.q = &q.pointer.queue
pc.Reset()
kq := &q.key.queue
q.key.queue.Reset()
var t f32.Affine2D
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeSave:
id := ops.DecodeSave(encOp.Data)
if extra := id - len(q.savedTrans) + 1; extra > 0 {
q.savedTrans = append(q.savedTrans, make([]f32.Affine2D, extra)...)
}
q.savedTrans[id] = t
case ops.TypeLoad:
id := ops.DecodeLoad(encOp.Data)
t = q.savedTrans[id]
pc.resetState()
pc.setTrans(t)
case ops.TypeClip:
var op ops.ClipOp
op.Decode(encOp.Data)
pc.clip(op)
case ops.TypePopClip:
pc.popArea()
case ops.TypeTransform:
t2, push := ops.DecodeTransform(encOp.Data)
if push {
q.transStack = append(q.transStack, t)
}
t = t.Mul(t2)
pc.setTrans(t)
case ops.TypePopTransform:
n := len(q.transStack)
t = q.transStack[n-1]
q.transStack = q.transStack[:n-1]
pc.setTrans(t)
case ops.TypeInput:
tag := encOp.Refs[0].(event.Tag)
s := q.stateFor(tag)
pc.inputOp(tag, &s.pointer)
a := pc.currentArea()
b := pc.currentAreaBounds()
if s.filter.focusable {
kq.inputOp(tag, &s.key, t, a, b)
}
// Pointer ops.
case ops.TypePass:
pc.pass()
case ops.TypePopPass:
pc.popPass()
case ops.TypeCursor:
name := pointer.Cursor(encOp.Data[1])
pc.cursor(name)
case ops.TypeActionInput:
act := system.Action(encOp.Data[1])
pc.actionInputOp(act)
case ops.TypeKeyInputHint:
op := key.InputHintOp{
Tag: encOp.Refs[0].(event.Tag),
Hint: key.InputHint(encOp.Data[1]),
}
s := q.stateFor(op.Tag)
s.key.inputHint(op.Hint)
// Semantic ops.
case ops.TypeSemanticLabel:
lbl := *encOp.Refs[0].(*string)
pc.semanticLabel(lbl)
case ops.TypeSemanticDesc:
desc := *encOp.Refs[0].(*string)
pc.semanticDesc(desc)
case ops.TypeSemanticClass:
class := semantic.ClassOp(encOp.Data[1])
pc.semanticClass(class)
case ops.TypeSemanticSelected:
if encOp.Data[1] != 0 {
pc.semanticSelected(true)
} else {
pc.semanticSelected(false)
}
case ops.TypeSemanticEnabled:
if encOp.Data[1] != 0 {
pc.semanticEnabled(true)
} else {
pc.semanticEnabled(false)
}
}
}
}
// WakeupTime returns the most recent time for doing another frame,
// as determined from the last call to Frame.
func (q *Router) WakeupTime() (time.Time, bool) {
t, w := q.wakeupTime, q.wakeup
q.wakeup = false
// Pending events always trigger wakeups.
if len(q.changes) > 1 || len(q.changes) == 1 && len(q.changes[0].events) > 0 {
t, w = time.Time{}, true
}
return t, w
}
func (s SemanticGestures) String() string {
var gestures []string
if s&ClickGesture != 0 {
gestures = append(gestures, "Click")
}
return strings.Join(gestures, ",")
}
func (SystemEvent) ImplementsEvent() {}
+34
View File
@@ -0,0 +1,34 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"testing"
"gioui.org/io/pointer"
"gioui.org/op"
)
func TestNoFilterAllocs(t *testing.T) {
b := testing.Benchmark(func(b *testing.B) {
var r Router
s := r.Source()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
s.Event(pointer.Filter{})
}
})
if allocs := b.AllocsPerOp(); allocs != 0 {
t.Fatalf("expected 0 AllocsPerOp, got %d", allocs)
}
}
func TestRouterWakeup(t *testing.T) {
r := new(Router)
r.Source().Execute(op.InvalidateCmd{})
r.Frame(new(op.Ops))
if _, wake := r.WakeupTime(); !wake {
t.Errorf("InvalidateCmd did not trigger a redraw")
}
}
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
package router package input
import ( import (
"fmt" "fmt"
@@ -9,6 +9,7 @@ import (
"testing" "testing"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/op" "gioui.org/op"
@@ -74,13 +75,19 @@ func TestSemanticTree(t *testing.T) {
func TestSemanticDescription(t *testing.T) { func TestSemanticDescription(t *testing.T) {
var ops op.Ops var ops op.Ops
pointer.InputOp{Tag: new(int), Types: pointer.Press | pointer.Release}.Add(&ops)
h := new(int)
event.Op(&ops, h)
semantic.DescriptionOp("description").Add(&ops) semantic.DescriptionOp("description").Add(&ops)
semantic.LabelOp("label").Add(&ops) semantic.LabelOp("label").Add(&ops)
semantic.Button.Add(&ops) semantic.Button.Add(&ops)
semantic.DisabledOp(true).Add(&ops) semantic.EnabledOp(false).Add(&ops)
semantic.SelectedOp(true).Add(&ops) semantic.SelectedOp(true).Add(&ops)
var r Router var r Router
events(&r, -1, pointer.Filter{
Target: h,
Kinds: pointer.Press | pointer.Release,
})
r.Frame(&ops) r.Frame(&ops)
tree := r.AppendSemantics(nil) tree := r.AppendSemantics(nil)
got := tree[0].Desc got := tree[0].Desc
+105 -220
View File
@@ -1,18 +1,9 @@
// SPDX-License-Identifier: Unlicense OR MIT // SPDX-License-Identifier: Unlicense OR MIT
/* // Package key implements key and text events and operations.
Package key implements key and text events and operations.
The InputOp operations is used for declaring key input handlers. Use
an implementation of the Queue interface from package ui to receive
events.
*/
package key package key
import ( import (
"encoding/binary"
"fmt"
"math"
"strings" "strings"
"gioui.org/f32" "gioui.org/f32"
@@ -21,59 +12,40 @@ import (
"gioui.org/op" "gioui.org/op"
) )
// InputOp declares a handler ready for key events. // Filter matches any [Event] that matches the parameters.
// Key events are in general only delivered to the type Filter struct {
// focused key handler. // Focus is the tag that must be focused for the filter to match. It has no effect
type InputOp struct { // if it is nil.
Tag event.Tag Focus event.Tag
// Hint describes the type of text expected by Tag. // Required is the set of modifiers that must be included in events matched.
Hint InputHint Required Modifiers
// Keys is the set of keys Tag can handle. That is, Tag will only // Optional is the set of modifiers that may be included in events matched.
// receive an Event if its key and modifiers are accepted by Keys.Contains. Optional Modifiers
// As a special case, the topmost (first added) InputOp handler receives all // Name of the key to be matched. As a special case, the empty
// unhandled events. // Name matches every key not matched by any other filter.
Keys Set Name Name
} }
// Set is an expression that describes a set of key combinations, in the form // InputHintOp describes the type of text expected by a tag.
// "<modifiers>-<keyset>|...". Modifiers are separated by dashes, optional type InputHintOp struct {
// modifiers are enclosed by parentheses. A key set is either a literal key Tag event.Tag
// name or a list of key names separated by commas and enclosed in brackets. Hint InputHint
// }
// The "Short" modifier matches the shortcut modifier (ModShortcut) and
// "ShortAlt" matches the alternative modifier (ModShortcutAlt).
//
// Examples:
//
// - A|B matches the A and B keys
// - [A,B] also matches the A and B keys
// - Shift-A matches A key if shift is pressed, and no other modifier.
// - Shift-(Ctrl)-A matches A if shift is pressed, and optionally ctrl.
type Set string
// SoftKeyboardOp shows or hide the on-screen keyboard, if available. // SoftKeyboardCmd shows or hides the on-screen keyboard, if available.
// It replaces any previous SoftKeyboardOp. type SoftKeyboardCmd struct {
type SoftKeyboardOp struct {
Show bool Show bool
} }
// FocusOp sets or clears the keyboard focus. It replaces any previous // SelectionCmd updates the selection for an input handler.
// FocusOp in the same frame. type SelectionCmd struct {
type FocusOp struct {
// Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
// has no InputOp in the same frame.
Tag event.Tag
}
// SelectionOp updates the selection for an input handler.
type SelectionOp struct {
Tag event.Tag Tag event.Tag
Range Range
Caret Caret
} }
// SnippetOp updates the content snippet for an input handler. // SnippetCmd updates the content snippet for an input handler.
type SnippetOp struct { type SnippetCmd struct {
Tag event.Tag Tag event.Tag
Snippet Snippet
} }
@@ -118,11 +90,8 @@ type FocusEvent struct {
// An Event is generated when a key is pressed. For text input // An Event is generated when a key is pressed. For text input
// use EditEvent. // use EditEvent.
type Event struct { type Event struct {
// Name of the key. For letters, the upper case form is used, via // Name of the key.
// unicode.ToUpper. The shift modifier is taken into account, all other Name Name
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
// combinations both give the Name "!" with the US keyboard layout.
Name string
// Modifiers is the set of active modifiers when the key was pressed. // Modifiers is the set of active modifiers when the key was pressed.
Modifiers Modifiers Modifiers Modifiers
// State is the state of the key when the event was fired. // State is the state of the key when the event was fired.
@@ -136,6 +105,13 @@ type EditEvent struct {
Text string Text string
} }
// FocusFilter matches any [FocusEvent], [EditEvent], [SnippetEvent],
// or [SelectionEvent] with the specified target.
type FocusFilter struct {
// Target is a tag specified in a previous event.Op.
Target event.Tag
}
// InputHint changes the on-screen-keyboard type. That hints the // InputHint changes the on-screen-keyboard type. That hints the
// type of data that might be entered by the user. // type of data that might be entered by the user.
type InputHint uint8 type InputHint uint8
@@ -189,41 +165,60 @@ const (
ModSuper ModSuper
) )
// Name is the identifier for a keyboard key.
//
// For letters, the upper case form is used, via unicode.ToUpper.
// The shift modifier is taken into account, all other
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
// combinations both give the Name "!" with the US keyboard layout.
type Name string
const ( const (
// Names for special keys. // Names for special keys.
NameLeftArrow = "←" NameLeftArrow Name = "←"
NameRightArrow = "→" NameRightArrow Name = "→"
NameUpArrow = "↑" NameUpArrow Name = "↑"
NameDownArrow = "↓" NameDownArrow Name = "↓"
NameReturn = "⏎" NameReturn Name = "⏎"
NameEnter = "⌤" NameEnter Name = "⌤"
NameEscape = "⎋" NameEscape Name = "⎋"
NameHome = "⇱" NameHome Name = "⇱"
NameEnd = "⇲" NameEnd Name = "⇲"
NameDeleteBackward = "⌫" NameDeleteBackward Name = "⌫"
NameDeleteForward = "⌦" NameDeleteForward Name = "⌦"
NamePageUp = "⇞" NamePageUp Name = "⇞"
NamePageDown = "⇟" NamePageDown Name = "⇟"
NameTab = "Tab" NameTab Name = "Tab"
NameSpace = "Space" NameSpace Name = "Space"
NameCtrl = "Ctrl" NameCtrl Name = "Ctrl"
NameShift = "Shift" NameShift Name = "Shift"
NameAlt = "Alt" NameAlt Name = "Alt"
NameSuper = "Super" NameSuper Name = "Super"
NameCommand = "⌘" NameCommand Name = "⌘"
NameF1 = "F1" NameF1 Name = "F1"
NameF2 = "F2" NameF2 Name = "F2"
NameF3 = "F3" NameF3 Name = "F3"
NameF4 = "F4" NameF4 Name = "F4"
NameF5 = "F5" NameF5 Name = "F5"
NameF6 = "F6" NameF6 Name = "F6"
NameF7 = "F7" NameF7 Name = "F7"
NameF8 = "F8" NameF8 Name = "F8"
NameF9 = "F9" NameF9 Name = "F9"
NameF10 = "F10" NameF10 Name = "F10"
NameF11 = "F11" NameF11 Name = "F11"
NameF12 = "F12" NameF12 Name = "F12"
NameBack = "Back" NameBack Name = "Back"
)
type FocusDirection int
const (
FocusRight FocusDirection = iota
FocusLeft
FocusUp
FocusDown
FocusForward
FocusBackward
) )
// Contain reports whether m contains all modifiers // Contain reports whether m contains all modifiers
@@ -232,162 +227,52 @@ func (m Modifiers) Contain(m2 Modifiers) bool {
return m&m2 == m2 return m&m2 == m2
} }
func (k Set) Contains(name string, mods Modifiers) bool { // FocusCmd requests to set or clear the keyboard focus.
ks := string(k) type FocusCmd struct {
for len(ks) > 0 { // Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
// Cut next key expression. // has no [event.Op] references.
chord, rest, _ := cut(ks, "|") Tag event.Tag
ks = rest
// Separate key set and modifier set.
var modSet, keySet string
sep := strings.LastIndex(chord, "-")
if sep != -1 {
modSet, keySet = chord[:sep], chord[sep+1:]
} else {
modSet, keySet = "", chord
}
if !keySetContains(keySet, name) {
continue
}
if modSetContains(modSet, mods) {
return true
}
}
return false
} }
func keySetContains(keySet, name string) bool { func (h InputHintOp) Add(o *op.Ops) {
// Check for single key match.
if keySet == name {
return true
}
// Check for set match.
if len(keySet) < 2 || keySet[0] != '[' || keySet[len(keySet)-1] != ']' {
return false
}
keySet = keySet[1 : len(keySet)-1]
for len(keySet) > 0 {
key, rest, _ := cut(keySet, ",")
keySet = rest
if key == name {
return true
}
}
return false
}
func modSetContains(modSet string, mods Modifiers) bool {
var smods Modifiers
for len(modSet) > 0 {
mod, rest, _ := cut(modSet, "-")
modSet = rest
if len(mod) >= 2 && mod[0] == '(' && mod[len(mod)-1] == ')' {
mods &^= modFor(mod[1 : len(mod)-1])
} else {
smods |= modFor(mod)
}
}
return mods == smods
}
// cut is a copy of the standard library strings.Cut.
// TODO: remove when Go 1.18 is our minimum.
func cut(s, sep string) (before, after string, found bool) {
if i := strings.Index(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, "", false
}
func modFor(name string) Modifiers {
switch name {
case NameCtrl:
return ModCtrl
case NameShift:
return ModShift
case NameAlt:
return ModAlt
case NameSuper:
return ModSuper
case NameCommand:
return ModCommand
case "Short":
return ModShortcut
case "ShortAlt":
return ModShortcutAlt
}
return 0
}
func (h InputOp) Add(o *op.Ops) {
if h.Tag == nil { if h.Tag == nil {
panic("Tag must be non-nil") panic("Tag must be non-nil")
} }
filter := h.Keys data := ops.Write1(&o.Internal, ops.TypeKeyInputHintLen, h.Tag)
data := ops.Write2(&o.Internal, ops.TypeKeyInputLen, h.Tag, &filter) data[0] = byte(ops.TypeKeyInputHint)
data[0] = byte(ops.TypeKeyInput)
data[1] = byte(h.Hint) data[1] = byte(h.Hint)
} }
func (h SoftKeyboardOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeKeySoftKeyboardLen)
data[0] = byte(ops.TypeKeySoftKeyboard)
if h.Show {
data[1] = 1
}
}
func (h FocusOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeKeyFocusLen, h.Tag)
data[0] = byte(ops.TypeKeyFocus)
}
func (s SnippetOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeSnippetLen, s.Tag, &s.Text)
data[0] = byte(ops.TypeSnippet)
bo := binary.LittleEndian
bo.PutUint32(data[1:], uint32(s.Range.Start))
bo.PutUint32(data[5:], uint32(s.Range.End))
}
func (s SelectionOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeSelectionLen, s.Tag)
data[0] = byte(ops.TypeSelection)
bo := binary.LittleEndian
bo.PutUint32(data[1:], uint32(s.Start))
bo.PutUint32(data[5:], uint32(s.End))
bo.PutUint32(data[9:], math.Float32bits(s.Pos.X))
bo.PutUint32(data[13:], math.Float32bits(s.Pos.Y))
bo.PutUint32(data[17:], math.Float32bits(s.Ascent))
bo.PutUint32(data[21:], math.Float32bits(s.Descent))
}
func (EditEvent) ImplementsEvent() {} func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {} func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {} func (FocusEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {} func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {} func (SelectionEvent) ImplementsEvent() {}
func (e Event) String() string { func (FocusCmd) ImplementsCommand() {}
return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State) func (SoftKeyboardCmd) ImplementsCommand() {}
} func (SelectionCmd) ImplementsCommand() {}
func (SnippetCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
func (FocusFilter) ImplementsFilter() {}
func (m Modifiers) String() string { func (m Modifiers) String() string {
var strs []string var strs []string
if m.Contain(ModCtrl) { if m.Contain(ModCtrl) {
strs = append(strs, NameCtrl) strs = append(strs, string(NameCtrl))
} }
if m.Contain(ModCommand) { if m.Contain(ModCommand) {
strs = append(strs, NameCommand) strs = append(strs, string(NameCommand))
} }
if m.Contain(ModShift) { if m.Contain(ModShift) {
strs = append(strs, NameShift) strs = append(strs, string(NameShift))
} }
if m.Contain(ModAlt) { if m.Contain(ModAlt) {
strs = append(strs, NameAlt) strs = append(strs, string(NameAlt))
} }
if m.Contain(ModSuper) { if m.Contain(ModSuper) {
strs = append(strs, NameSuper) strs = append(strs, string(NameSuper))
} }
return strings.Join(strs, "-") return strings.Join(strs, "-")
} }
-35
View File
@@ -1,35 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package key
import (
"testing"
)
func TestKeySet(t *testing.T) {
const allMods = ModAlt | ModShift | ModSuper | ModCtrl | ModCommand
tests := []struct {
Set Set
Matches []Event
Mismatches []Event
}{
{"A", []Event{{Name: "A"}}, []Event{{Name: "A", Modifiers: ModShift}}},
{"[A,B,C]", []Event{{Name: "A"}, {Name: "B"}}, []Event{}},
{"Short-A", []Event{{Name: "A", Modifiers: ModShortcut}}, []Event{{Name: "A", Modifiers: ModShift}}},
{"(Ctrl)-A", []Event{{Name: "A", Modifiers: ModCtrl}, {Name: "A"}}, []Event{{Name: "A", Modifiers: ModShift}}},
{"Shift-[A,B,C]", []Event{{Name: "A", Modifiers: ModShift}}, []Event{{Name: "B", Modifiers: ModShift | ModCtrl}}},
{Set(allMods.String() + "-A"), []Event{{Name: "A", Modifiers: allMods}}, []Event{}},
}
for _, tst := range tests {
for _, e := range tst.Matches {
if !tst.Set.Contains(e.Name, e.Modifiers) {
t.Errorf("key set %q didn't contain %+v", tst.Set, e)
}
}
for _, e := range tst.Mismatches {
if tst.Set.Contains(e.Name, e.Modifiers) {
t.Errorf("key set %q contains %+v", tst.Set, e)
}
}
}
}
+7 -24
View File
@@ -5,36 +5,19 @@ Package pointer implements pointer events and operations.
A pointer is either a mouse controlled cursor or a touch A pointer is either a mouse controlled cursor or a touch
object such as a finger. object such as a finger.
The InputOp operation is used to declare a handler ready for pointer The [event.Op] operation is used to declare a handler ready for pointer
events. Use an event.Queue to receive events. events.
# Types
Only events that match a specified list of types are delivered to a handler.
For example, to receive Press, Drag, and Release events (but not Move, Enter,
Leave, or Scroll):
var ops op.Ops
var h *Handler = ...
pointer.InputOp{
Tag: h,
Types: pointer.Press | pointer.Drag | pointer.Release,
}.Add(ops)
Cancel events are always delivered.
# Hit areas # Hit areas
Clip operations from package op/clip are used for specifying Clip operations from package [op/clip] are used for specifying
hit areas where subsequent InputOps are active. hit areas where handlers may receive events.
For example, to set up a handler with a rectangular hit area: For example, to set up a handler with a rectangular hit area:
r := image.Rectangle{...} r := image.Rectangle{...}
area := clip.Rect(r).Push(ops) area := clip.Rect(r).Push(ops)
pointer.InputOp{Tag: h}.Add(ops) event.Op{Tag: h}.Add(ops)
area.Pop() area.Pop()
Note that hit areas behave similar to painting: the effective area of a stack Note that hit areas behave similar to painting: the effective area of a stack
@@ -54,11 +37,11 @@ For example:
var h1, h2 *Handler var h1, h2 *Handler
area := clip.Rect(...).Push(ops) area := clip.Rect(...).Push(ops)
pointer.InputOp{Tag: h1}.Add(Ops) event.Op(Ops, h1)
area.Pop() area.Pop()
area := clip.Rect(...).Push(ops) area := clip.Rect(...).Push(ops)
pointer.InputOp{Tag: h2}.Add(ops) event.Op(Ops, h2)
area.Pop() area.Pop()
implies a tree of two inner nodes, each with one pointer handler attached. implies a tree of two inner nodes, each with one pointer handler attached.
+48 -51
View File
@@ -3,9 +3,6 @@
package pointer package pointer
import ( import (
"encoding/binary"
"fmt"
"image"
"strings" "strings"
"time" "time"
@@ -18,7 +15,7 @@ import (
// Event is a pointer event. // Event is a pointer event.
type Event struct { type Event struct {
Type Type Kind Kind
Source Source Source Source
// PointerID is the id for the pointer and can be used // PointerID is the id for the pointer and can be used
// to track a particular pointer from Press to // to track a particular pointer from Press to
@@ -32,8 +29,10 @@ type Event struct {
Time time.Duration Time time.Duration
// Buttons are the set of pressed mouse buttons for this event. // Buttons are the set of pressed mouse buttons for this event.
Buttons Buttons Buttons Buttons
// Position is the position of the event, relative to // Position is the coordinates of the event in the local coordinate
// the current transformation, as set by op.TransformOp. // system of the receiving tag. The transformation from global window
// coordinates to local coordinates is performed by the inverse of
// the effective transformation of the tag.
Position f32.Point Position f32.Point
// Scroll is the scroll amount, if any. // Scroll is the scroll amount, if any.
Scroll f32.Point Scroll f32.Point
@@ -51,30 +50,41 @@ type PassOp struct {
type PassStack struct { type PassStack struct {
ops *ops.Ops ops *ops.Ops
id ops.StackID id ops.StackID
macroID int macroID uint32
} }
// InputOp declares an input handler ready for pointer // Filter matches every [Event] that target the Tag and whose kind is
// events. // included in Kinds. Note that only tags specified in [event.Op] can
type InputOp struct { // be targeted by pointer events.
Tag event.Tag type Filter struct {
// Grab, if set, request that the handler get Target event.Tag
// Grabbed priority. // Kinds is a bitwise-or of event types to match.
Grab bool Kinds Kind
// Types is a bitwise-or of event types to receive. // ScrollX and ScrollY constrain the range of scrolling events delivered
Types Type // to Target. Specifically, any Event e delivered to Tag will satisfy
// ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy
// //
// ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis) // ScrollX.Min <= e.Scroll.X <= ScrollX.Max (horizontal axis)
// ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis) // ScrollY.Min <= e.Scroll.Y <= ScrollY.Max (vertical axis)
ScrollBounds image.Rectangle ScrollX ScrollRange
ScrollY ScrollRange
}
// ScrollRange describes the range of scrolling distances in an
// axis.
type ScrollRange struct {
Min, Max int
}
// GrabCmd requests a pointer grab on the pointer identified by ID.
type GrabCmd struct {
Tag event.Tag
ID ID
} }
type ID uint16 type ID uint16
// Type of an Event. // Kind of an Event.
type Type uint type Kind uint
// Priority of an Event. // Priority of an Event.
type Priority uint8 type Priority uint8
@@ -169,7 +179,7 @@ const (
const ( const (
// A Cancel event is generated when the current gesture is // A Cancel event is generated when the current gesture is
// interrupted by other handlers or the system. // interrupted by other handlers or the system.
Cancel Type = (1 << iota) >> 1 Cancel Kind = 1 << iota
// Press of a pointer. // Press of a pointer.
Press Press
// Release of a pointer. // Release of a pointer.
@@ -215,6 +225,13 @@ const (
ButtonTertiary ButtonTertiary
) )
func (s ScrollRange) Union(s2 ScrollRange) ScrollRange {
return ScrollRange{
Min: min(s.Min, s2.Min),
Max: max(s.Max, s2.Max),
}
}
// Push the current pass mode to the pass stack and set the pass mode. // Push the current pass mode to the pass stack and set the pass mode.
func (p PassOp) Push(o *op.Ops) PassStack { func (p PassOp) Push(o *op.Ops) PassStack {
id, mid := ops.PushOp(&o.Internal, ops.PassStack) id, mid := ops.PushOp(&o.Internal, ops.PassStack)
@@ -235,36 +252,12 @@ func (op Cursor) Add(o *op.Ops) {
data[1] = byte(op) data[1] = byte(op)
} }
// Add panics if the scroll range does not contain zero. func (t Kind) String() string {
func (op InputOp) Add(o *op.Ops) {
if op.Tag == nil {
panic("Tag must be non-nil")
}
if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 {
panic(fmt.Errorf("invalid scroll range value %v", b))
}
if op.Types>>16 > 0 {
panic(fmt.Errorf("value in Types overflows uint16"))
}
data := ops.Write1(&o.Internal, ops.TypePointerInputLen, op.Tag)
data[0] = byte(ops.TypePointerInput)
if op.Grab {
data[1] = 1
}
bo := binary.LittleEndian
bo.PutUint16(data[2:], uint16(op.Types))
bo.PutUint32(data[4:], uint32(op.ScrollBounds.Min.X))
bo.PutUint32(data[8:], uint32(op.ScrollBounds.Min.Y))
bo.PutUint32(data[12:], uint32(op.ScrollBounds.Max.X))
bo.PutUint32(data[16:], uint32(op.ScrollBounds.Max.Y))
}
func (t Type) String() string {
if t == Cancel { if t == Cancel {
return "Cancel" return "Cancel"
} }
var buf strings.Builder var buf strings.Builder
for tt := Type(1); tt > 0; tt <<= 1 { for tt := Kind(1); tt > 0; tt <<= 1 {
if t&tt > 0 { if t&tt > 0 {
if buf.Len() > 0 { if buf.Len() > 0 {
buf.WriteByte('|') buf.WriteByte('|')
@@ -275,7 +268,7 @@ func (t Type) String() string {
return buf.String() return buf.String()
} }
func (t Type) string() string { func (t Kind) string() string {
switch t { switch t {
case Press: case Press:
return "Press" return "Press"
@@ -402,3 +395,7 @@ func (c Cursor) String() string {
} }
func (Event) ImplementsEvent() {} func (Event) ImplementsEvent() {}
func (GrabCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
func TestTypeString(t *testing.T) { func TestTypeString(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
typ Type typ Kind
res string res string
}{ }{
{Cancel, "Cancel"}, {Cancel, "Cancel"},
-31
View File
@@ -1,31 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package profiles provides access to rendering
// profiles.
package profile
import (
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/op"
)
// Op registers a handler for receiving
// Events.
type Op struct {
Tag event.Tag
}
// Event contains profile data from a single
// rendered frame.
type Event struct {
// Timings. Very likely to change.
Timings string
}
func (p Op) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeProfileLen, p.Tag)
data[0] = byte(ops.TypeProfile)
}
func (p Event) ImplementsEvent() {}
-57
View File
@@ -1,57 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package router
import (
"gioui.org/io/event"
)
type clipboardQueue struct {
receivers map[event.Tag]struct{}
// request avoid read clipboard every frame while waiting.
requested bool
text *string
}
// WriteClipboard returns the most recent text to be copied
// to the clipboard, if any.
func (q *clipboardQueue) WriteClipboard() (string, bool) {
if q.text == nil {
return "", false
}
text := *q.text
q.text = nil
return text, true
}
// ReadClipboard reports if any new handler is waiting
// to read the clipboard.
func (q *clipboardQueue) ReadClipboard() bool {
if len(q.receivers) == 0 || q.requested {
return false
}
q.requested = true
return true
}
func (q *clipboardQueue) Push(e event.Event, events *handlerEvents) {
for r := range q.receivers {
events.Add(r, e)
delete(q.receivers, r)
}
}
func (q *clipboardQueue) ProcessWriteClipboard(refs []interface{}) {
q.text = refs[0].(*string)
}
func (q *clipboardQueue) ProcessReadClipboard(refs []interface{}) {
if q.receivers == nil {
q.receivers = make(map[event.Tag]struct{})
}
tag := refs[0].(event.Tag)
if _, ok := q.receivers[tag]; !ok {
q.receivers[tag] = struct{}{}
q.requested = false
}
}
-154
View File
@@ -1,154 +0,0 @@
package router
import (
"testing"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/op"
)
func TestClipboardDuplicateEvent(t *testing.T) {
ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
// Both must receive the event once
clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
clipboard.ReadOp{Tag: &handler[1]}.Add(ops)
router.Frame(ops)
event := clipboard.Event{Text: "Test"}
router.Queue(event)
assertClipboardReadOp(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), true)
assertClipboardEvent(t, router.Events(&handler[1]), true)
ops.Reset()
// No ReadOp
router.Frame(ops)
assertClipboardReadOp(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), false)
assertClipboardEvent(t, router.Events(&handler[1]), false)
ops.Reset()
clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
router.Frame(ops)
// No ClipboardEvent sent
assertClipboardReadOp(t, router, 1)
assertClipboardEvent(t, router.Events(&handler[0]), false)
assertClipboardEvent(t, router.Events(&handler[1]), false)
ops.Reset()
}
func TestQueueProcessReadClipboard(t *testing.T) {
ops, router, handler := new(op.Ops), new(Router), make([]int, 2)
ops.Reset()
// Request read
clipboard.ReadOp{Tag: &handler[0]}.Add(ops)
router.Frame(ops)
assertClipboardReadOp(t, router, 1)
ops.Reset()
for i := 0; i < 3; i++ {
// No ReadOp
// One receiver must still wait for response
router.Frame(ops)
assertClipboardReadOpDuplicated(t, router, 1)
ops.Reset()
}
router.Frame(ops)
// Send the clipboard event
event := clipboard.Event{Text: "Text 2"}
router.Queue(event)
assertClipboardReadOp(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), true)
ops.Reset()
// No ReadOp
// There's no receiver waiting
router.Frame(ops)
assertClipboardReadOp(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), false)
ops.Reset()
}
func TestQueueProcessWriteClipboard(t *testing.T) {
ops, router := new(op.Ops), new(Router)
ops.Reset()
clipboard.WriteOp{Text: "Write 1"}.Add(ops)
router.Frame(ops)
assertClipboardWriteOp(t, router, "Write 1")
ops.Reset()
// No WriteOp
router.Frame(ops)
assertClipboardWriteOp(t, router, "")
ops.Reset()
clipboard.WriteOp{Text: "Write 2"}.Add(ops)
router.Frame(ops)
assertClipboardReadOp(t, router, 0)
assertClipboardWriteOp(t, router, "Write 2")
ops.Reset()
}
func assertClipboardEvent(t *testing.T, events []event.Event, expected bool) {
t.Helper()
var evtClipboard int
for _, e := range events {
switch e.(type) {
case clipboard.Event:
evtClipboard++
}
}
if evtClipboard <= 0 && expected {
t.Error("expected to receive some event")
}
if evtClipboard > 0 && !expected {
t.Error("unexpected event received")
}
}
func assertClipboardReadOp(t *testing.T, router *Router, expected int) {
t.Helper()
if len(router.cqueue.receivers) != expected {
t.Error("unexpected number of receivers")
}
if router.cqueue.ReadClipboard() != (expected > 0) {
t.Error("missing requests")
}
}
func assertClipboardReadOpDuplicated(t *testing.T, router *Router, expected int) {
t.Helper()
if len(router.cqueue.receivers) != expected {
t.Error("receivers removed")
}
if router.cqueue.ReadClipboard() != false {
t.Error("duplicated requests")
}
}
func assertClipboardWriteOp(t *testing.T, router *Router, expected string) {
t.Helper()
if (router.cqueue.text != nil) != (expected != "") {
t.Error("text not defined")
}
text, ok := router.cqueue.WriteClipboard()
if ok != (expected != "") {
t.Error("duplicated requests")
}
if text != expected {
t.Errorf("got text %s, expected %s", text, expected)
}
}
-353
View File
@@ -1,353 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package router
import (
"image"
"sort"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/key"
)
// EditorState represents the state of an editor needed by input handlers.
type EditorState struct {
Selection struct {
Transform f32.Affine2D
key.Range
key.Caret
}
Snippet key.Snippet
}
type TextInputState uint8
type keyQueue struct {
focus event.Tag
order []event.Tag
dirOrder []dirFocusEntry
handlers map[event.Tag]*keyHandler
state TextInputState
hint key.InputHint
content EditorState
}
type keyHandler struct {
// visible will be true if the InputOp is present
// in the current frame.
visible bool
new bool
hint key.InputHint
order int
dirOrder int
filter key.Set
}
// keyCollector tracks state required to update a keyQueue
// from key ops.
type keyCollector struct {
q *keyQueue
focus event.Tag
changed bool
}
type dirFocusEntry struct {
tag event.Tag
row int
area int
bounds image.Rectangle
}
const (
TextInputKeep TextInputState = iota
TextInputClose
TextInputOpen
)
type FocusDirection int
const (
FocusRight FocusDirection = iota
FocusLeft
FocusUp
FocusDown
FocusForward
FocusBackward
)
// InputState returns the last text input state as
// determined in Frame.
func (q *keyQueue) InputState() TextInputState {
state := q.state
q.state = TextInputKeep
return state
}
// InputHint returns the input mode from the most recent key.InputOp.
func (q *keyQueue) InputHint() (key.InputHint, bool) {
if q.focus == nil {
return q.hint, false
}
focused, ok := q.handlers[q.focus]
if !ok {
return q.hint, false
}
old := q.hint
q.hint = focused.hint
return q.hint, old != q.hint
}
func (q *keyQueue) Reset() {
if q.handlers == nil {
q.handlers = make(map[event.Tag]*keyHandler)
}
for _, h := range q.handlers {
h.visible, h.new = false, false
h.order = -1
}
q.order = q.order[:0]
q.dirOrder = q.dirOrder[:0]
}
func (q *keyQueue) Frame(events *handlerEvents, collector keyCollector) {
changed, focus := collector.changed, collector.focus
for k, h := range q.handlers {
if !h.visible {
delete(q.handlers, k)
if q.focus == k {
// Remove focus from the handler that is no longer visible.
q.focus = nil
q.state = TextInputClose
}
} else if h.new && k != focus {
// Reset the handler on (each) first appearance, but don't trigger redraw.
events.AddNoRedraw(k, key.FocusEvent{Focus: false})
}
}
if changed {
q.setFocus(focus, events)
}
q.updateFocusLayout()
}
// updateFocusLayout partitions input handlers handlers into rows
// for directional focus moves.
//
// The approach is greedy: pick the topmost handler and create a row
// containing it. Then, extend the handler bounds to a horizontal beam
// and add to the row every handler whose center intersect it. Repeat
// until no handlers remain.
func (q *keyQueue) updateFocusLayout() {
order := q.dirOrder
// Sort by ascending y position.
sort.SliceStable(order, func(i, j int) bool {
return order[i].bounds.Min.Y < order[j].bounds.Min.Y
})
row := 0
for len(order) > 0 {
h := &order[0]
h.row = row
bottom := h.bounds.Max.Y
end := 1
for ; end < len(order); end++ {
h := &order[end]
center := (h.bounds.Min.Y + h.bounds.Max.Y) / 2
if center > bottom {
break
}
h.row = row
}
// Sort row by ascending x position.
sort.SliceStable(order[:end], func(i, j int) bool {
return order[i].bounds.Min.X < order[j].bounds.Min.X
})
order = order[end:]
row++
}
for i, o := range q.dirOrder {
q.handlers[o.tag].dirOrder = i
}
}
// MoveFocus attempts to move the focus in the direction of dir, returning true if it succeeds.
func (q *keyQueue) MoveFocus(dir FocusDirection, events *handlerEvents) bool {
if len(q.dirOrder) == 0 {
return false
}
order := 0
if q.focus != nil {
order = q.handlers[q.focus].dirOrder
}
focus := q.dirOrder[order]
switch dir {
case FocusForward, FocusBackward:
if len(q.order) == 0 {
break
}
order := 0
if dir == FocusBackward {
order = -1
}
if q.focus != nil {
order = q.handlers[q.focus].order
if dir == FocusForward {
order++
} else {
order--
}
}
order = (order + len(q.order)) % len(q.order)
q.setFocus(q.order[order], events)
return true
case FocusRight, FocusLeft:
next := order
if q.focus != nil {
next = order + 1
if dir == FocusLeft {
next = order - 1
}
}
if 0 <= next && next < len(q.dirOrder) {
newFocus := q.dirOrder[next]
if newFocus.row == focus.row {
q.setFocus(newFocus.tag, events)
return true
}
}
case FocusUp, FocusDown:
delta := +1
if dir == FocusUp {
delta = -1
}
nextRow := 0
if q.focus != nil {
nextRow = focus.row + delta
}
var closest event.Tag
dist := int(1e6)
center := (focus.bounds.Min.X + focus.bounds.Max.X) / 2
loop:
for 0 <= order && order < len(q.dirOrder) {
next := q.dirOrder[order]
switch next.row {
case nextRow:
nextCenter := (next.bounds.Min.X + next.bounds.Max.X) / 2
d := center - nextCenter
if d < 0 {
d = -d
}
if d > dist {
break loop
}
dist = d
closest = next.tag
case nextRow + delta:
break loop
}
order += delta
}
if closest != nil {
q.setFocus(closest, events)
return true
}
}
return false
}
func (q *keyQueue) BoundsFor(t event.Tag) image.Rectangle {
order := q.handlers[t].dirOrder
return q.dirOrder[order].bounds
}
func (q *keyQueue) AreaFor(t event.Tag) int {
order := q.handlers[t].dirOrder
return q.dirOrder[order].area
}
func (q *keyQueue) Accepts(t event.Tag, e key.Event) bool {
return q.handlers[t].filter.Contains(e.Name, e.Modifiers)
}
func (q *keyQueue) setFocus(focus event.Tag, events *handlerEvents) {
if focus != nil {
if _, exists := q.handlers[focus]; !exists {
focus = nil
}
}
if focus == q.focus {
return
}
q.content = EditorState{}
if q.focus != nil {
events.Add(q.focus, key.FocusEvent{Focus: false})
}
q.focus = focus
if q.focus != nil {
events.Add(q.focus, key.FocusEvent{Focus: true})
}
if q.focus == nil || q.state == TextInputKeep {
q.state = TextInputClose
}
}
func (k *keyCollector) focusOp(tag event.Tag) {
k.focus = tag
k.changed = true
}
func (k *keyCollector) softKeyboard(show bool) {
if show {
k.q.state = TextInputOpen
} else {
k.q.state = TextInputClose
}
}
func (k *keyCollector) handlerFor(tag event.Tag, area int, bounds image.Rectangle) *keyHandler {
h, ok := k.q.handlers[tag]
if !ok {
h = &keyHandler{new: true, order: -1}
k.q.handlers[tag] = h
}
if h.order == -1 {
h.order = len(k.q.order)
k.q.order = append(k.q.order, tag)
k.q.dirOrder = append(k.q.dirOrder, dirFocusEntry{tag: tag, area: area, bounds: bounds})
}
return h
}
func (k *keyCollector) inputOp(op key.InputOp, area int, bounds image.Rectangle) {
h := k.handlerFor(op.Tag, area, bounds)
h.visible = true
h.hint = op.Hint
h.filter = op.Keys
}
func (k *keyCollector) selectionOp(t f32.Affine2D, op key.SelectionOp) {
if op.Tag == k.q.focus {
k.q.content.Selection.Range = op.Range
k.q.content.Selection.Caret = op.Caret
k.q.content.Selection.Transform = t
}
}
func (k *keyCollector) snippetOp(op key.SnippetOp) {
if op.Tag == k.q.focus {
k.q.content.Snippet = op.Snippet
}
}
func (t TextInputState) String() string {
switch t {
case TextInputKeep:
return "Keep"
case TextInputClose:
return "Close"
case TextInputOpen:
return "Open"
default:
panic("unexpected value")
}
}
-432
View File
@@ -1,432 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package router
import (
"image"
"reflect"
"testing"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/op"
"gioui.org/op/clip"
)
func TestKeyWakeup(t *testing.T) {
handler := new(int)
var ops op.Ops
key.InputOp{Tag: handler}.Add(&ops)
var r Router
// Test that merely adding a handler doesn't trigger redraw.
r.Frame(&ops)
if _, wake := r.WakeupTime(); wake {
t.Errorf("adding key.InputOp triggered a redraw")
}
// However, adding a handler queues a Focus(false) event.
if evts := r.Events(handler); len(evts) != 1 {
t.Errorf("no Focus event for newly registered key.InputOp")
}
// Verify that r.Events does trigger a redraw.
r.Frame(&ops)
if _, wake := r.WakeupTime(); !wake {
t.Errorf("key.FocusEvent event didn't trigger a redraw")
}
}
func TestKeyMultiples(t *testing.T) {
handlers := make([]int, 3)
ops := new(op.Ops)
r := new(Router)
key.SoftKeyboardOp{Show: true}.Add(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.FocusOp{Tag: &handlers[2]}.Add(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
// The last one must be focused:
key.InputOp{Tag: &handlers[2]}.Add(ops)
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), false)
assertKeyEvent(t, r.Events(&handlers[1]), false)
assertKeyEvent(t, r.Events(&handlers[2]), true)
assertFocus(t, r, &handlers[2])
assertKeyboard(t, r, TextInputOpen)
}
func TestKeyStacked(t *testing.T) {
handlers := make([]int, 4)
ops := new(op.Ops)
r := new(Router)
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.FocusOp{Tag: nil}.Add(ops)
key.SoftKeyboardOp{Show: false}.Add(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
key.FocusOp{Tag: &handlers[1]}.Add(ops)
key.InputOp{Tag: &handlers[2]}.Add(ops)
key.SoftKeyboardOp{Show: true}.Add(ops)
key.InputOp{Tag: &handlers[3]}.Add(ops)
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), false)
assertKeyEvent(t, r.Events(&handlers[1]), true)
assertKeyEvent(t, r.Events(&handlers[2]), false)
assertKeyEvent(t, r.Events(&handlers[3]), false)
assertFocus(t, r, &handlers[1])
assertKeyboard(t, r, TextInputOpen)
}
func TestKeySoftKeyboardNoFocus(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
// It's possible to open the keyboard
// without any active focus:
key.SoftKeyboardOp{Show: true}.Add(ops)
r.Frame(ops)
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputOpen)
}
func TestKeyRemoveFocus(t *testing.T) {
handlers := make([]int, 2)
ops := new(op.Ops)
r := new(Router)
// New InputOp with Focus and Keyboard:
key.InputOp{Tag: &handlers[0], Keys: "Short-Tab"}.Add(ops)
key.FocusOp{Tag: &handlers[0]}.Add(ops)
key.SoftKeyboardOp{Show: true}.Add(ops)
// New InputOp without any focus:
key.InputOp{Tag: &handlers[1], Keys: "Short-Tab"}.Add(ops)
r.Frame(ops)
// Add some key events:
event := event.Event(key.Event{Name: key.NameTab, Modifiers: key.ModShortcut, State: key.Press})
r.Queue(event)
assertKeyEvent(t, r.Events(&handlers[0]), true, event)
assertKeyEvent(t, r.Events(&handlers[1]), false)
assertFocus(t, r, &handlers[0])
assertKeyboard(t, r, TextInputOpen)
ops.Reset()
// Will get the focus removed:
key.InputOp{Tag: &handlers[0]}.Add(ops)
// Unchanged:
key.InputOp{Tag: &handlers[1]}.Add(ops)
// Remove focus by focusing on a tag that don't exist.
key.FocusOp{Tag: new(int)}.Add(ops)
r.Frame(ops)
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose)
ops.Reset()
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.InputOp{Tag: &handlers[1]}.Add(ops)
r.Frame(ops)
assertKeyEventUnexpected(t, r.Events(&handlers[0]))
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose)
ops.Reset()
// Set focus to InputOp which already
// exists in the previous frame:
key.FocusOp{Tag: &handlers[0]}.Add(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.SoftKeyboardOp{Show: true}.Add(ops)
// Remove focus.
key.InputOp{Tag: &handlers[1]}.Add(ops)
key.FocusOp{Tag: nil}.Add(ops)
r.Frame(ops)
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputOpen)
}
func TestKeyFocusedInvisible(t *testing.T) {
handlers := make([]int, 2)
ops := new(op.Ops)
r := new(Router)
// Set new InputOp with focus:
key.FocusOp{Tag: &handlers[0]}.Add(ops)
key.InputOp{Tag: &handlers[0]}.Add(ops)
key.SoftKeyboardOp{Show: true}.Add(ops)
// Set new InputOp without focus:
key.InputOp{Tag: &handlers[1]}.Add(ops)
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), true)
assertKeyEvent(t, r.Events(&handlers[1]), false)
assertFocus(t, r, &handlers[0])
assertKeyboard(t, r, TextInputOpen)
ops.Reset()
//
// Removed first (focused) element!
//
// Unchanged:
key.InputOp{Tag: &handlers[1]}.Add(ops)
r.Frame(ops)
assertKeyEventUnexpected(t, r.Events(&handlers[0]))
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose)
ops.Reset()
// Respawn the first element:
// It must receive one `Event{Focus: false}`.
key.InputOp{Tag: &handlers[0]}.Add(ops)
// Unchanged
key.InputOp{Tag: &handlers[1]}.Add(ops)
r.Frame(ops)
assertKeyEvent(t, r.Events(&handlers[0]), false)
assertKeyEventUnexpected(t, r.Events(&handlers[1]))
assertFocus(t, r, nil)
assertKeyboard(t, r, TextInputClose)
}
func TestNoOps(t *testing.T) {
r := new(Router)
r.Frame(nil)
}
func TestDirectionalFocus(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
handlers := []image.Rectangle{
image.Rect(10, 10, 50, 50),
image.Rect(50, 20, 100, 80),
image.Rect(20, 26, 60, 80),
image.Rect(10, 60, 50, 100),
}
for i, bounds := range handlers {
cl := clip.Rect(bounds).Push(ops)
key.InputOp{Tag: &handlers[i]}.Add(ops)
cl.Pop()
}
r.Frame(ops)
r.MoveFocus(FocusLeft)
assertFocus(t, r, &handlers[0])
r.MoveFocus(FocusLeft)
assertFocus(t, r, &handlers[0])
r.MoveFocus(FocusRight)
assertFocus(t, r, &handlers[1])
r.MoveFocus(FocusRight)
assertFocus(t, r, &handlers[1])
r.MoveFocus(FocusDown)
assertFocus(t, r, &handlers[2])
r.MoveFocus(FocusDown)
assertFocus(t, r, &handlers[2])
r.MoveFocus(FocusLeft)
assertFocus(t, r, &handlers[3])
r.MoveFocus(FocusUp)
assertFocus(t, r, &handlers[0])
r.MoveFocus(FocusForward)
assertFocus(t, r, &handlers[1])
r.MoveFocus(FocusBackward)
assertFocus(t, r, &handlers[0])
}
func TestFocusScroll(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
h := new(int)
parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops)
cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops)
key.InputOp{Tag: h}.Add(ops)
pointer.InputOp{
Tag: h,
Types: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100),
}.Add(ops)
// Test that h is scrolled even if behind another handler.
pointer.InputOp{
Tag: new(int),
}.Add(ops)
cl.Pop()
parent.Pop()
r.Frame(ops)
r.MoveFocus(FocusLeft)
r.RevealFocus(image.Rect(0, 0, 15, 40))
evts := r.Events(h)
assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9))
}
func TestFocusClick(t *testing.T) {
ops := new(op.Ops)
r := new(Router)
h := new(int)
cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops)
key.InputOp{Tag: h}.Add(ops)
pointer.InputOp{
Tag: h,
Types: pointer.Press | pointer.Release,
}.Add(ops)
cl.Pop()
r.Frame(ops)
r.MoveFocus(FocusLeft)
r.ClickFocus()
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press, pointer.Release)
}
func TestNoFocus(t *testing.T) {
r := new(Router)
r.MoveFocus(FocusForward)
}
func TestKeyRouting(t *testing.T) {
handlers := make([]int, 5)
ops := new(op.Ops)
macroOps := new(op.Ops)
r := new(Router)
rect := clip.Rect{Max: image.Pt(10, 10)}
macro := op.Record(macroOps)
key.InputOp{Tag: &handlers[0], Keys: "A"}.Add(ops)
cl1 := rect.Push(ops)
key.InputOp{Tag: &handlers[1], Keys: "B"}.Add(ops)
key.InputOp{Tag: &handlers[2], Keys: "A"}.Add(ops)
cl1.Pop()
cl2 := rect.Push(ops)
key.InputOp{Tag: &handlers[3]}.Add(ops)
key.InputOp{Tag: &handlers[4], Keys: "A"}.Add(ops)
cl2.Pop()
call := macro.Stop()
call.Add(ops)
r.Frame(ops)
A, B := key.Event{Name: "A"}, key.Event{Name: "B"}
r.Queue(A, B)
// With no focus, the events should traverse the final branch of the hit tree
// searching for handlers.
assertKeyEvent(t, r.Events(&handlers[4]), false, A)
assertKeyEvent(t, r.Events(&handlers[3]), false)
assertKeyEvent(t, r.Events(&handlers[2]), false)
assertKeyEvent(t, r.Events(&handlers[1]), false, B)
assertKeyEvent(t, r.Events(&handlers[0]), false)
r2 := new(Router)
call.Add(ops)
key.FocusOp{Tag: &handlers[3]}.Add(ops)
r2.Frame(ops)
r2.Queue(A, B)
// With focus, the events should traverse the branch of the hit tree
// containing the focused element.
assertKeyEvent(t, r2.Events(&handlers[4]), false)
assertKeyEvent(t, r2.Events(&handlers[3]), true)
assertKeyEvent(t, r2.Events(&handlers[2]), false)
assertKeyEvent(t, r2.Events(&handlers[1]), false)
assertKeyEvent(t, r2.Events(&handlers[0]), false, A)
}
func assertKeyEvent(t *testing.T, events []event.Event, expectedFocus bool, expectedInputs ...event.Event) {
t.Helper()
var evtFocus int
var evtKeyPress int
for _, e := range events {
switch ev := e.(type) {
case key.FocusEvent:
if ev.Focus != expectedFocus {
t.Errorf("focus is expected to be %v, got %v", expectedFocus, ev.Focus)
}
evtFocus++
case key.Event, key.EditEvent:
if len(expectedInputs) <= evtKeyPress {
t.Fatalf("unexpected key events")
}
if !reflect.DeepEqual(ev, expectedInputs[evtKeyPress]) {
t.Errorf("expected %v events, got %v", expectedInputs[evtKeyPress], ev)
}
evtKeyPress++
}
}
if evtFocus <= 0 {
t.Errorf("expected focus event")
}
if evtFocus > 1 {
t.Errorf("expected single focus event")
}
if evtKeyPress != len(expectedInputs) {
t.Errorf("expected key events")
}
}
func assertKeyEventUnexpected(t *testing.T, events []event.Event) {
t.Helper()
var evtFocus int
for _, e := range events {
switch e.(type) {
case key.FocusEvent:
evtFocus++
}
}
if evtFocus > 1 {
t.Errorf("unexpected focus event")
}
}
func assertFocus(t *testing.T, router *Router, expected event.Tag) {
t.Helper()
if got := router.key.queue.focus; got != expected {
t.Errorf("expected %v to be focused, got %v", expected, got)
}
}
func assertKeyboard(t *testing.T, router *Router, expected TextInputState) {
t.Helper()
if got := router.key.queue.state; got != expected {
t.Errorf("expected %v keyboard, got %v", expected, got)
}
}
File diff suppressed because it is too large Load Diff
-644
View File
@@ -1,644 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
/*
Package router implements Router, a event.Queue implementation
that that disambiguates and routes events to handlers declared
in operation lists.
Router is used by app.Window and is otherwise only useful for
using Gio with external window implementations.
*/
package router
import (
"encoding/binary"
"image"
"io"
"math"
"strings"
"time"
"gioui.org/f32"
f32internal "gioui.org/internal/f32"
"gioui.org/internal/ops"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/profile"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
)
// Router is a Queue implementation that routes events
// to handlers declared in operation lists.
type Router struct {
savedTrans []f32.Affine2D
transStack []f32.Affine2D
pointer struct {
queue pointerQueue
collector pointerCollector
}
key struct {
queue keyQueue
collector keyCollector
}
cqueue clipboardQueue
handlers handlerEvents
reader ops.Reader
// InvalidateOp summary.
wakeup bool
wakeupTime time.Time
// ProfileOp summary.
profHandlers map[event.Tag]struct{}
profile profile.Event
}
// SemanticNode represents a node in the tree describing the components
// contained in a frame.
type SemanticNode struct {
ID SemanticID
ParentID SemanticID
Children []SemanticNode
Desc SemanticDesc
areaIdx int
}
// SemanticDesc provides a semantic description of a UI component.
type SemanticDesc struct {
Class semantic.ClassOp
Description string
Label string
Selected bool
Disabled bool
Gestures SemanticGestures
Bounds image.Rectangle
}
// SemanticGestures is a bit-set of supported gestures.
type SemanticGestures int
const (
ClickGesture SemanticGestures = 1 << iota
ScrollGesture
)
// SemanticID uniquely identifies a SemanticDescription.
//
// By convention, the zero value denotes the non-existent ID.
type SemanticID uint64
type handlerEvents struct {
handlers map[event.Tag][]event.Event
hadEvents bool
}
// Events returns the available events for the handler key.
func (q *Router) Events(k event.Tag) []event.Event {
events := q.handlers.Events(k)
if _, isprof := q.profHandlers[k]; isprof {
delete(q.profHandlers, k)
events = append(events, q.profile)
}
return events
}
// Frame replaces the declared handlers from the supplied
// operation list. The text input state, wakeup time and whether
// there are active profile handlers is also saved.
func (q *Router) Frame(frame *op.Ops) {
q.handlers.Clear()
q.wakeup = false
for k := range q.profHandlers {
delete(q.profHandlers, k)
}
var ops *ops.Ops
if frame != nil {
ops = &frame.Internal
}
q.reader.Reset(ops)
q.collect()
q.pointer.queue.Frame(&q.handlers)
q.key.queue.Frame(&q.handlers, q.key.collector)
if q.handlers.HadEvents() {
q.wakeup = true
q.wakeupTime = time.Time{}
}
}
// Queue key events to the topmost handler.
func (q *Router) QueueTopmost(events ...key.Event) bool {
var topmost event.Tag
pq := &q.pointer.queue
for _, h := range pq.hitTree {
if h.ktag != nil {
topmost = h.ktag
break
}
}
if topmost == nil {
return false
}
for _, e := range events {
q.handlers.Add(topmost, e)
}
return q.handlers.HadEvents()
}
// Queue events and report whether at least one handler had an event queued.
func (q *Router) Queue(events ...event.Event) bool {
for _, e := range events {
switch e := e.(type) {
case profile.Event:
q.profile = e
case pointer.Event:
q.pointer.queue.Push(e, &q.handlers)
case key.Event:
q.queueKeyEvent(e)
case key.SnippetEvent:
// Expand existing, overlapping snippet.
if r := q.key.queue.content.Snippet.Range; rangeOverlaps(r, key.Range(e)) {
if e.Start > r.Start {
e.Start = r.Start
}
if e.End < r.End {
e.End = r.End
}
}
if f := q.key.queue.focus; f != nil {
q.handlers.Add(f, e)
}
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
if f := q.key.queue.focus; f != nil {
q.handlers.Add(f, e)
}
case clipboard.Event:
q.cqueue.Push(e, &q.handlers)
}
}
return q.handlers.HadEvents()
}
func rangeOverlaps(r1, r2 key.Range) bool {
r1 = rangeNorm(r1)
r2 = rangeNorm(r2)
return r1.Start <= r2.Start && r2.Start < r1.End ||
r1.Start <= r2.End && r2.End < r1.End
}
func rangeNorm(r key.Range) key.Range {
if r.End < r.Start {
r.End, r.Start = r.Start, r.End
}
return r
}
func (q *Router) queueKeyEvent(e key.Event) {
kq := &q.key.queue
f := q.key.queue.focus
if f != nil && kq.Accepts(f, e) {
q.handlers.Add(f, e)
return
}
pq := &q.pointer.queue
idx := len(pq.hitTree) - 1
focused := f != nil
if focused {
// If there is a focused tag, traverse its ancestry through the
// hit tree to search for handlers.
for ; pq.hitTree[idx].ktag != f; idx-- {
}
}
for idx != -1 {
n := &pq.hitTree[idx]
if focused {
idx = n.next
} else {
idx--
}
if n.ktag == nil {
continue
}
if kq.Accepts(n.ktag, e) {
q.handlers.Add(n.ktag, e)
break
}
}
}
func (q *Router) MoveFocus(dir FocusDirection) bool {
return q.key.queue.MoveFocus(dir, &q.handlers)
}
// RevealFocus scrolls the current focus (if any) into viewport
// if there are scrollable parent handlers.
func (q *Router) RevealFocus(viewport image.Rectangle) {
focus := q.key.queue.focus
if focus == nil {
return
}
bounds := q.key.queue.BoundsFor(focus)
area := q.key.queue.AreaFor(focus)
viewport = q.pointer.queue.ClipFor(area, viewport)
topleft := bounds.Min.Sub(viewport.Min)
topleft = max(topleft, bounds.Max.Sub(viewport.Max))
topleft = min(image.Pt(0, 0), topleft)
bottomright := bounds.Max.Sub(viewport.Max)
bottomright = min(bottomright, bounds.Min.Sub(viewport.Min))
bottomright = max(image.Pt(0, 0), bottomright)
s := topleft
if s.X == 0 {
s.X = bottomright.X
}
if s.Y == 0 {
s.Y = bottomright.Y
}
q.ScrollFocus(s)
}
// ScrollFocus scrolls the focused widget, if any, by dist.
func (q *Router) ScrollFocus(dist image.Point) {
focus := q.key.queue.focus
if focus == nil {
return
}
area := q.key.queue.AreaFor(focus)
q.pointer.queue.Deliver(area, pointer.Event{
Type: pointer.Scroll,
Source: pointer.Touch,
Scroll: f32internal.FPt(dist),
}, &q.handlers)
}
func max(p1, p2 image.Point) image.Point {
m := p1
if p2.X > m.X {
m.X = p2.X
}
if p2.Y > m.Y {
m.Y = p2.Y
}
return m
}
func min(p1, p2 image.Point) image.Point {
m := p1
if p2.X < m.X {
m.X = p2.X
}
if p2.Y < m.Y {
m.Y = p2.Y
}
return m
}
func (q *Router) ActionAt(p f32.Point) (system.Action, bool) {
return q.pointer.queue.ActionAt(p)
}
func (q *Router) ClickFocus() {
focus := q.key.queue.focus
if focus == nil {
return
}
bounds := q.key.queue.BoundsFor(focus)
center := bounds.Max.Add(bounds.Min).Div(2)
e := pointer.Event{
Position: f32.Pt(float32(center.X), float32(center.Y)),
Source: pointer.Touch,
}
area := q.key.queue.AreaFor(focus)
e.Type = pointer.Press
q.pointer.queue.Deliver(area, e, &q.handlers)
e.Type = pointer.Release
q.pointer.queue.Deliver(area, e, &q.handlers)
}
// TextInputState returns the input state from the most recent
// call to Frame.
func (q *Router) TextInputState() TextInputState {
return q.key.queue.InputState()
}
// TextInputHint returns the input mode from the most recent key.InputOp.
func (q *Router) TextInputHint() (key.InputHint, bool) {
return q.key.queue.InputHint()
}
// WriteClipboard returns the most recent text to be copied
// to the clipboard, if any.
func (q *Router) WriteClipboard() (string, bool) {
return q.cqueue.WriteClipboard()
}
// ReadClipboard reports if any new handler is waiting
// to read the clipboard.
func (q *Router) ReadClipboard() bool {
return q.cqueue.ReadClipboard()
}
// Cursor returns the last cursor set.
func (q *Router) Cursor() pointer.Cursor {
return q.pointer.queue.cursor
}
// SemanticAt returns the first semantic description under pos, if any.
func (q *Router) SemanticAt(pos f32.Point) (SemanticID, bool) {
return q.pointer.queue.SemanticAt(pos)
}
// AppendSemantics appends the semantic tree to nodes, and returns the result.
// The root node is the first added.
func (q *Router) AppendSemantics(nodes []SemanticNode) []SemanticNode {
q.pointer.collector.q = &q.pointer.queue
q.pointer.collector.ensureRoot()
return q.pointer.queue.AppendSemantics(nodes)
}
// EditorState returns the editor state for the focused handler, or the
// zero value if there is none.
func (q *Router) EditorState() EditorState {
return q.key.queue.content
}
func (q *Router) collect() {
q.transStack = q.transStack[:0]
pc := &q.pointer.collector
pc.q = &q.pointer.queue
pc.reset()
kc := &q.key.collector
*kc = keyCollector{q: &q.key.queue}
q.key.queue.Reset()
var t f32.Affine2D
bo := binary.LittleEndian
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeInvalidate:
op := decodeInvalidateOp(encOp.Data)
if !q.wakeup || op.At.Before(q.wakeupTime) {
q.wakeup = true
q.wakeupTime = op.At
}
case ops.TypeProfile:
op := decodeProfileOp(encOp.Data, encOp.Refs)
if q.profHandlers == nil {
q.profHandlers = make(map[event.Tag]struct{})
}
q.profHandlers[op.Tag] = struct{}{}
case ops.TypeClipboardRead:
q.cqueue.ProcessReadClipboard(encOp.Refs)
case ops.TypeClipboardWrite:
q.cqueue.ProcessWriteClipboard(encOp.Refs)
case ops.TypeSave:
id := ops.DecodeSave(encOp.Data)
if extra := id - len(q.savedTrans) + 1; extra > 0 {
q.savedTrans = append(q.savedTrans, make([]f32.Affine2D, extra)...)
}
q.savedTrans[id] = t
case ops.TypeLoad:
id := ops.DecodeLoad(encOp.Data)
t = q.savedTrans[id]
pc.resetState()
pc.setTrans(t)
case ops.TypeClip:
var op ops.ClipOp
op.Decode(encOp.Data)
pc.clip(op)
case ops.TypePopClip:
pc.popArea()
case ops.TypeTransform:
t2, push := ops.DecodeTransform(encOp.Data)
if push {
q.transStack = append(q.transStack, t)
}
t = t.Mul(t2)
pc.setTrans(t)
case ops.TypePopTransform:
n := len(q.transStack)
t = q.transStack[n-1]
q.transStack = q.transStack[:n-1]
pc.setTrans(t)
// Pointer ops.
case ops.TypePass:
pc.pass()
case ops.TypePopPass:
pc.popPass()
case ops.TypePointerInput:
op := pointer.InputOp{
Tag: encOp.Refs[0].(event.Tag),
Grab: encOp.Data[1] != 0,
Types: pointer.Type(bo.Uint16(encOp.Data[2:])),
ScrollBounds: image.Rectangle{
Min: image.Point{
X: int(int32(bo.Uint32(encOp.Data[4:]))),
Y: int(int32(bo.Uint32(encOp.Data[8:]))),
},
Max: image.Point{
X: int(int32(bo.Uint32(encOp.Data[12:]))),
Y: int(int32(bo.Uint32(encOp.Data[16:]))),
},
},
}
pc.inputOp(op, &q.handlers)
case ops.TypeCursor:
name := pointer.Cursor(encOp.Data[1])
pc.cursor(name)
case ops.TypeSource:
op := transfer.SourceOp{
Tag: encOp.Refs[0].(event.Tag),
Type: encOp.Refs[1].(string),
}
pc.sourceOp(op, &q.handlers)
case ops.TypeTarget:
op := transfer.TargetOp{
Tag: encOp.Refs[0].(event.Tag),
Type: encOp.Refs[1].(string),
}
pc.targetOp(op, &q.handlers)
case ops.TypeOffer:
op := transfer.OfferOp{
Tag: encOp.Refs[0].(event.Tag),
Type: encOp.Refs[1].(string),
Data: encOp.Refs[2].(io.ReadCloser),
}
pc.offerOp(op, &q.handlers)
case ops.TypeActionInput:
act := system.Action(encOp.Data[1])
pc.actionInputOp(act)
// Key ops.
case ops.TypeKeyFocus:
tag, _ := encOp.Refs[0].(event.Tag)
op := key.FocusOp{
Tag: tag,
}
kc.focusOp(op.Tag)
case ops.TypeKeySoftKeyboard:
op := key.SoftKeyboardOp{
Show: encOp.Data[1] != 0,
}
kc.softKeyboard(op.Show)
case ops.TypeKeyInput:
filter := encOp.Refs[1].(*key.Set)
op := key.InputOp{
Tag: encOp.Refs[0].(event.Tag),
Hint: key.InputHint(encOp.Data[1]),
Keys: *filter,
}
a := pc.currentArea()
b := pc.currentAreaBounds()
pc.keyInputOp(op)
kc.inputOp(op, a, b)
case ops.TypeSnippet:
op := key.SnippetOp{
Tag: encOp.Refs[0].(event.Tag),
Snippet: key.Snippet{
Range: key.Range{
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
End: int(int32(bo.Uint32(encOp.Data[5:]))),
},
Text: *(encOp.Refs[1].(*string)),
},
}
kc.snippetOp(op)
case ops.TypeSelection:
op := key.SelectionOp{
Tag: encOp.Refs[0].(event.Tag),
Range: key.Range{
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
End: int(int32(bo.Uint32(encOp.Data[5:]))),
},
Caret: key.Caret{
Pos: f32.Point{
X: math.Float32frombits(bo.Uint32(encOp.Data[9:])),
Y: math.Float32frombits(bo.Uint32(encOp.Data[13:])),
},
Ascent: math.Float32frombits(bo.Uint32(encOp.Data[17:])),
Descent: math.Float32frombits(bo.Uint32(encOp.Data[21:])),
},
}
kc.selectionOp(t, op)
// Semantic ops.
case ops.TypeSemanticLabel:
lbl := encOp.Refs[0].(string)
pc.semanticLabel(lbl)
case ops.TypeSemanticDesc:
desc := encOp.Refs[0].(string)
pc.semanticDesc(desc)
case ops.TypeSemanticClass:
class := semantic.ClassOp(encOp.Data[1])
pc.semanticClass(class)
case ops.TypeSemanticSelected:
if encOp.Data[1] != 0 {
pc.semanticSelected(true)
} else {
pc.semanticSelected(false)
}
case ops.TypeSemanticDisabled:
if encOp.Data[1] != 0 {
pc.semanticDisabled(true)
} else {
pc.semanticDisabled(false)
}
}
}
}
// Profiling reports whether there was profile handlers in the
// most recent Frame call.
func (q *Router) Profiling() bool {
return len(q.profHandlers) > 0
}
// WakeupTime returns the most recent time for doing another frame,
// as determined from the last call to Frame.
func (q *Router) WakeupTime() (time.Time, bool) {
return q.wakeupTime, q.wakeup
}
func (h *handlerEvents) init() {
if h.handlers == nil {
h.handlers = make(map[event.Tag][]event.Event)
}
}
func (h *handlerEvents) AddNoRedraw(k event.Tag, e event.Event) {
h.init()
h.handlers[k] = append(h.handlers[k], e)
}
func (h *handlerEvents) Add(k event.Tag, e event.Event) {
h.AddNoRedraw(k, e)
h.hadEvents = true
}
func (h *handlerEvents) HadEvents() bool {
u := h.hadEvents
h.hadEvents = false
return u
}
func (h *handlerEvents) Events(k event.Tag) []event.Event {
if events, ok := h.handlers[k]; ok {
h.handlers[k] = h.handlers[k][:0]
// Schedule another frame if we delivered events to the user
// to flush half-updated state. This is important when an
// event changes UI state that has already been laid out. In
// the worst case, we waste a frame, increasing power usage.
//
// Gio is expected to grow the ability to construct
// frame-to-frame differences and only render to changed
// areas. In that case, the waste of a spurious frame should
// be minimal.
h.hadEvents = h.hadEvents || len(events) > 0
return events
}
return nil
}
func (h *handlerEvents) Clear() {
for k := range h.handlers {
delete(h.handlers, k)
}
}
func decodeProfileOp(d []byte, refs []interface{}) profile.Op {
if ops.OpType(d[0]) != ops.TypeProfile {
panic("invalid op")
}
return profile.Op{
Tag: refs[0].(event.Tag),
}
}
func decodeInvalidateOp(d []byte) op.InvalidateOp {
bo := binary.LittleEndian
if ops.OpType(d[0]) != ops.TypeInvalidate {
panic("invalid op")
}
var o op.InvalidateOp
if nanos := bo.Uint64(d[1:]); nanos > 0 {
o.At = time.Unix(0, int64(nanos))
}
return o
}
func (s SemanticGestures) String() string {
var gestures []string
if s&ClickGesture != 0 {
gestures = append(gestures, "Click")
}
return strings.Join(gestures, ",")
}
+8 -8
View File
@@ -36,16 +36,16 @@ const (
// boolean state. // boolean state.
type SelectedOp bool type SelectedOp bool
// DisabledOp describes the disabled state. // EnabledOp describes the enabled state.
type DisabledOp bool type EnabledOp bool
func (l LabelOp) Add(o *op.Ops) { func (l LabelOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeSemanticLabelLen, string(l)) data := ops.Write1String(&o.Internal, ops.TypeSemanticLabelLen, string(l))
data[0] = byte(ops.TypeSemanticLabel) data[0] = byte(ops.TypeSemanticLabel)
} }
func (d DescriptionOp) Add(o *op.Ops) { func (d DescriptionOp) Add(o *op.Ops) {
data := ops.Write1(&o.Internal, ops.TypeSemanticDescLen, string(d)) data := ops.Write1String(&o.Internal, ops.TypeSemanticDescLen, string(d))
data[0] = byte(ops.TypeSemanticDesc) data[0] = byte(ops.TypeSemanticDesc)
} }
@@ -63,10 +63,10 @@ func (s SelectedOp) Add(o *op.Ops) {
} }
} }
func (d DisabledOp) Add(o *op.Ops) { func (e EnabledOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeSemanticDisabledLen) data := ops.Write(&o.Internal, ops.TypeSemanticEnabledLen)
data[0] = byte(ops.TypeSemanticDisabled) data[0] = byte(ops.TypeSemanticEnabled)
if d { if e {
data[1] = 1 data[1] = 1
} }
} }
-89
View File
@@ -1,89 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
// Package system contains events usually handled at the top-level
// program level.
package system
import (
"image"
"time"
"gioui.org/io/event"
"gioui.org/op"
"gioui.org/unit"
)
// A FrameEvent requests a new frame in the form of a list of
// operations that describes what to display and how to handle
// input.
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)
// Queue supplies the events for event handlers.
Queue event.Queue
}
// 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
}
// 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
}
// A StageEvent is generated whenever the stage of a
// Window changes.
type StageEvent struct {
Stage Stage
}
// Stage of a Window.
type Stage uint8
const (
// StagePaused is the stage for windows that have no on-screen representation.
// Paused windows don't receive FrameEvent.
StagePaused Stage = iota
// StageInactive is the stage for windows that are visible, but not active.
// Inactive windows receive FrameEvent.
StageInactive
// StageRunning is for active and visible Windows.
// Running windows receive FrameEvent.
StageRunning
)
// String implements fmt.Stringer.
func (l Stage) String() string {
switch l {
case StagePaused:
return "StagePaused"
case StageInactive:
return "StageInactive"
case StageRunning:
return "StageRunning"
default:
panic("unexpected Stage value")
}
}
func (FrameEvent) ImplementsEvent() {}
func (StageEvent) ImplementsEvent() {}
func (DestroyEvent) ImplementsEvent() {}
+29 -43
View File
@@ -2,11 +2,11 @@
// //
// The transfer protocol is as follows: // The transfer protocol is as follows:
// //
// - Data sources are registered with SourceOps, data targets with TargetOps. // - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag
// - A data source receives a RequestEvent when a transfer is initiated. // is initiated, and an [RequestEvent] for each initiation of a data transfer.
// It must respond with an OfferOp. // Sources respond to requests with [OfferCmd].
// - The target receives a DataEvent when transferring to it. It must close // - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data.
// the event data after use. // The target must close the data event after use.
// //
// When a user initiates a pointer-guided drag and drop transfer, the // When a user initiates a pointer-guided drag and drop transfer, the
// source as well as all potential targets receive an InitiateEvent. // source as well as all potential targets receive an InitiateEvent.
@@ -20,29 +20,11 @@ package transfer
import ( import (
"io" "io"
"gioui.org/internal/ops"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/op"
) )
// SourceOp registers a tag as a data source for a MIME type. // OfferCmd is used by data sources as a response to a RequestEvent.
// Use multiple SourceOps if a tag supports multiple types. type OfferCmd struct {
type SourceOp struct {
Tag event.Tag
// Type is the MIME type supported by this source.
Type string
}
// TargetOp registers a tag as a data target.
// Use multiple TargetOps if a tag supports multiple types.
type TargetOp struct {
Tag event.Tag
// Type is the MIME type accepted by this target.
Type string
}
// OfferOp is used by data sources as a response to a RequestEvent.
type OfferOp struct {
Tag event.Tag Tag event.Tag
// Type is the MIME type of Data. // Type is the MIME type of Data.
// It must be the Type from the corresponding RequestEvent. // It must be the Type from the corresponding RequestEvent.
@@ -50,32 +32,33 @@ type OfferOp struct {
// Data contains the offered data. It is closed when the // Data contains the offered data. It is closed when the
// transfer is complete or cancelled. // transfer is complete or cancelled.
// Data must be kept valid until closed, and it may be used from // Data must be kept valid until closed, and it may be used from
// a goroutine separate from the one processing the frame.. // a goroutine separate from the one processing the frame.
Data io.ReadCloser Data io.ReadCloser
} }
func (op SourceOp) Add(o *op.Ops) { func (OfferCmd) ImplementsCommand() {}
data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type)
data[0] = byte(ops.TypeSource) // SourceFilter filters for any [RequestEvent] that match a MIME type
// as well as [InitiateEvent] and [CancelEvent].
// Use multiple filters to offer multiple types.
type SourceFilter struct {
// Target is a tag included in a previous event.Op.
Target event.Tag
// Type is the MIME type supported by this source.
Type string
} }
func (op TargetOp) Add(o *op.Ops) { // TargetFilter filters for any [DataEvent] whose type matches a MIME type
data := ops.Write2(&o.Internal, ops.TypeTargetLen, op.Tag, op.Type) // as well as [CancelEvent]. Use multiple filters to accept multiple types.
data[0] = byte(ops.TypeTarget) type TargetFilter struct {
} // Target is a tag included in a previous event.Op.
Target event.Tag
// Add the offer to the list of operations. // Type is the MIME type accepted by this target.
// It panics if the Data field is not set. Type string
func (op OfferOp) Add(o *op.Ops) {
if op.Data == nil {
panic("invalid nil data in OfferOp")
}
data := ops.Write3(&o.Internal, ops.TypeOfferLen, op.Tag, op.Type, op.Data)
data[0] = byte(ops.TypeOffer)
} }
// RequestEvent requests data from a data source. The source must // RequestEvent requests data from a data source. The source must
// respond with an OfferOp. // respond with an OfferCmd.
type RequestEvent struct { type RequestEvent struct {
// Type is the first matched type between the source and the target. // Type is the first matched type between the source and the target.
Type string Type string
@@ -107,3 +90,6 @@ type DataEvent struct {
} }
func (DataEvent) ImplementsEvent() {} func (DataEvent) ImplementsEvent() {}
func (SourceFilter) ImplementsFilter() {}
func (TargetFilter) ImplementsFilter() {}
+5 -57
View File
@@ -3,10 +3,9 @@
package layout package layout
import ( import (
"image"
"time" "time"
"gioui.org/io/event" "gioui.org/io/input"
"gioui.org/io/system" "gioui.org/io/system"
"gioui.org/op" "gioui.org/op"
"gioui.org/unit" "gioui.org/unit"
@@ -21,9 +20,6 @@ type Context struct {
Constraints Constraints Constraints Constraints
Metric unit.Metric Metric unit.Metric
// By convention, a nil Queue is a signal to widgets to draw themselves
// in a disabled state.
Queue event.Queue
// Now is the animation time. // Now is the animation time.
Now time.Time Now time.Time
@@ -32,46 +28,10 @@ type Context struct {
// Interested users must look up and populate these values manually. // Interested users must look up and populate these values manually.
Locale system.Locale Locale system.Locale
input.Source
*op.Ops *op.Ops
} }
// NewContext is a shorthand for
//
// Context{
// Ops: ops,
// Now: e.Now,
// Queue: e.Queue,
// Config: e.Config,
// Constraints: Exact(e.Size),
// }
//
// NewContext calls ops.Reset and adjusts ops for e.Insets.
func NewContext(ops *op.Ops, e system.FrameEvent) Context {
ops.Reset()
size := e.Size
if e.Insets != (system.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 Context{
Ops: ops,
Now: e.Now,
Queue: e.Queue,
Metric: e.Metric,
Constraints: Exact(size),
}
}
// Dp converts v to pixels. // Dp converts v to pixels.
func (c Context) Dp(v unit.Dp) int { func (c Context) Dp(v unit.Dp) int {
return c.Metric.Dp(v) return c.Metric.Dp(v)
@@ -82,21 +42,9 @@ func (c Context) Sp(v unit.Sp) int {
return c.Metric.Sp(v) return c.Metric.Sp(v)
} }
// Events returns the events available for the key. If no // Disabled returns a copy of this context with a disabled Source,
// queue is configured, Events returns nil. // blocking widgets from changing its state and receiving events.
func (c Context) Events(k event.Tag) []event.Event {
if c.Queue == nil {
return nil
}
return c.Queue.Events(k)
}
// Disabled returns a copy of this context with a nil Queue,
// blocking events to widgets using it.
//
// By convention, a nil Queue is a signal to widgets to draw themselves
// in a disabled state.
func (c Context) Disabled() Context { func (c Context) Disabled() Context {
c.Queue = nil c.Source = input.Source{}
return c return c
} }
+24
View File
@@ -103,6 +103,30 @@ func ExampleStack() {
// Expand: {(50,50) (100,100)} // Expand: {(50,50) (100,100)}
} }
func ExampleBackground() {
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Constraints{
Max: image.Point{X: 100, Y: 100},
},
}
layout.Background{}.Layout(gtx,
// Force widget to the same size as the second.
func(gtx layout.Context) layout.Dimensions {
fmt.Printf("Expand: %v\n", gtx.Constraints)
return layoutWidget(gtx, 10, 10)
},
// Rigid 50x50 widget.
func(gtx layout.Context) layout.Dimensions {
return layoutWidget(gtx, 50, 50)
},
)
// Output:
// Expand: {(50,50) (100,100)}
}
func ExampleList() { func ExampleList() {
gtx := layout.Context{ gtx := layout.Context{
Ops: new(op.Ops), Ops: new(op.Ops),
+22 -20
View File
@@ -7,6 +7,7 @@ import (
"math" "math"
"gioui.org/gesture" "gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
) )
@@ -144,7 +145,26 @@ func (l *List) Dragging() bool {
} }
func (l *List) update(gtx Context) { func (l *List) update(gtx Context) {
d := l.scroll.Scroll(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis)) min, max := int(-inf), int(inf)
if l.Position.First == 0 {
// Use the size of the invisible part as scroll boundary.
min = -l.Position.Offset
if min > 0 {
min = 0
}
}
if l.Position.First+l.Position.Count == l.len {
max = -l.Position.OffsetLast
if max < 0 {
max = 0
}
}
xrange := pointer.ScrollRange{Min: min, Max: max}
yrange := pointer.ScrollRange{}
if l.Axis == Vertical {
xrange, yrange = yrange, xrange
}
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), xrange, yrange)
l.scrollDelta = d l.scrollDelta = d
l.Position.Offset += d l.Position.Offset += d
} }
@@ -332,25 +352,7 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
call := macro.Stop() call := macro.Stop()
defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop() defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop()
min, max := int(-inf), int(inf) l.scroll.Add(ops)
if l.Position.First == 0 {
// Use the size of the invisible part as scroll boundary.
min = -l.Position.Offset
if min > 0 {
min = 0
}
}
if l.Position.First+l.Position.Count == l.len {
max = -l.Position.OffsetLast
if max < 0 {
max = 0
}
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
}
l.scroll.Add(ops, scrollRange)
call.Add(ops) call.Add(ops)
return Dimensions{Size: dims} return Dimensions{Size: dims}
+12 -12
View File
@@ -8,8 +8,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"
) )
@@ -64,13 +64,13 @@ func TestListScrollToEnd(t *testing.T) {
func TestListPosition(t *testing.T) { func TestListPosition(t *testing.T) {
_s := func(e ...event.Event) []event.Event { return e } _s := func(e ...event.Event) []event.Event { return e }
r := new(router.Router) r := new(input.Router)
gtx := Context{ gtx := Context{
Ops: new(op.Ops), Ops: new(op.Ops),
Constraints: Constraints{ Constraints: Constraints{
Max: image.Pt(20, 10), Max: image.Pt(20, 10),
}, },
Queue: r, Source: r.Source(),
} }
el := func(gtx Context, idx int) Dimensions { el := func(gtx Context, idx int) Dimensions {
return Dimensions{Size: image.Pt(10, 10)} return Dimensions{Size: image.Pt(10, 10)}
@@ -93,18 +93,18 @@ func TestListPosition(t *testing.T) {
pointer.Event{ pointer.Event{
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
Type: pointer.Press, Kind: pointer.Press,
Position: f32.Pt(0, 0), Position: f32.Pt(0, 0),
}, },
pointer.Event{ pointer.Event{
Source: pointer.Mouse, Source: pointer.Mouse,
Type: pointer.Scroll, Kind: pointer.Scroll,
Scroll: f32.Pt(5, 0), Scroll: f32.Pt(5, 0),
}, },
pointer.Event{ pointer.Event{
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
Type: pointer.Release, Kind: pointer.Release,
Position: f32.Pt(5, 0), Position: f32.Pt(5, 0),
}, },
)}, )},
@@ -113,18 +113,18 @@ func TestListPosition(t *testing.T) {
pointer.Event{ pointer.Event{
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
Type: pointer.Press, Kind: pointer.Press,
Position: f32.Pt(0, 0), Position: f32.Pt(0, 0),
}, },
pointer.Event{ pointer.Event{
Source: pointer.Mouse, Source: pointer.Mouse,
Type: pointer.Scroll, Kind: pointer.Scroll,
Scroll: f32.Pt(3, 0), Scroll: f32.Pt(3, 0),
}, },
pointer.Event{ pointer.Event{
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
Type: pointer.Release, Kind: pointer.Release,
Position: f32.Pt(5, 0), Position: f32.Pt(5, 0),
}, },
)}, )},
@@ -133,18 +133,18 @@ func TestListPosition(t *testing.T) {
pointer.Event{ pointer.Event{
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
Type: pointer.Press, Kind: pointer.Press,
Position: f32.Pt(0, 0), Position: f32.Pt(0, 0),
}, },
pointer.Event{ pointer.Event{
Source: pointer.Mouse, Source: pointer.Mouse,
Type: pointer.Scroll, Kind: pointer.Scroll,
Scroll: f32.Pt(10, 0), Scroll: f32.Pt(10, 0),
}, },
pointer.Event{ pointer.Event{
Source: pointer.Mouse, Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary, Buttons: pointer.ButtonPrimary,
Type: pointer.Release, Kind: pointer.Release,
Position: f32.Pt(15, 0), Position: f32.Pt(15, 0),
}, },
)}, )},
+33
View File
@@ -118,3 +118,36 @@ func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
Baseline: baseline, Baseline: baseline,
} }
} }
// Background lays out single child widget on top of a background,
// centering, if necessary.
type Background struct{}
// Layout a widget and then add a background to it.
func (Background) Layout(gtx Context, background, widget Widget) Dimensions {
macro := op.Record(gtx.Ops)
wdims := widget(gtx)
baseline := wdims.Baseline
call := macro.Stop()
cgtx := gtx
cgtx.Constraints.Min = gtx.Constraints.Constrain(wdims.Size)
bdims := background(cgtx)
if bdims.Size != wdims.Size {
p := image.Point{
X: (bdims.Size.X - wdims.Size.X) / 2,
Y: (bdims.Size.Y - wdims.Size.Y) / 2,
}
baseline += (bdims.Size.Y - wdims.Size.Y) / 2
trans := op.Offset(p).Push(gtx.Ops)
defer trans.Pop()
}
call.Add(gtx.Ops)
return Dimensions{
Size: bdims.Size,
Baseline: baseline,
}
}
+66
View File
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: Unlicense OR MIT
package layout
import (
"image"
"testing"
"gioui.org/op"
)
func BenchmarkStack(b *testing.B) {
gtx := Context{
Ops: new(op.Ops),
Constraints: Constraints{
Max: image.Point{X: 100, Y: 100},
},
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
gtx.Ops.Reset()
Stack{}.Layout(gtx,
Expanded(emptyWidget{
Size: image.Point{X: 60, Y: 60},
}.Layout),
Stacked(emptyWidget{
Size: image.Point{X: 30, Y: 30},
}.Layout),
)
}
}
func BenchmarkBackground(b *testing.B) {
gtx := Context{
Ops: new(op.Ops),
Constraints: Constraints{
Max: image.Point{X: 100, Y: 100},
},
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
gtx.Ops.Reset()
Background{}.Layout(gtx,
emptyWidget{
Size: image.Point{X: 60, Y: 60},
}.Layout,
emptyWidget{
Size: image.Point{X: 30, Y: 30},
}.Layout,
)
}
}
type emptyWidget struct {
Size image.Point
}
func (w emptyWidget) Layout(gtx Context) Dimensions {
return Dimensions{Size: w.Size}
}
+10 -1
View File
@@ -29,7 +29,7 @@ type Op struct {
type Stack struct { type Stack struct {
ops *ops.Ops ops *ops.Ops
id ops.StackID id ops.StackID
macroID int macroID uint32
} }
var pathSeed maphash.Seed var pathSeed maphash.Seed
@@ -138,6 +138,9 @@ type Path struct {
func (p *Path) Pos() f32.Point { return p.pen } func (p *Path) Pos() f32.Point { return p.pen }
// Begin the path, storing the path data and final Op into ops. // Begin the path, storing the path data and final Op into ops.
//
// Caller must also call End to finish the drawing.
// Forgetting to call it will result in a "panic: cannot mix multi ops with single ones".
func (p *Path) Begin(o *op.Ops) { func (p *Path) Begin(o *op.Ops) {
*p = Path{ *p = Path{
ops: &o.Internal, ops: &o.Internal,
@@ -204,6 +207,9 @@ func (p *Path) Line(delta f32.Point) {
// LineTo moves the pen to the absolute point specified, recording a line. // LineTo moves the pen to the absolute point specified, recording a line.
func (p *Path) LineTo(to f32.Point) { func (p *Path) LineTo(to f32.Point) {
if to == p.pen {
return
}
data := ops.WriteMulti(p.ops, scene.CommandSize+4) data := ops.WriteMulti(p.ops, scene.CommandSize+4)
bo := binary.LittleEndian bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour)) bo.PutUint32(data[0:], uint32(p.contour))
@@ -250,6 +256,9 @@ func (p *Path) Quad(ctrl, to f32.Point) {
// QuadTo records a quadratic Bézier from the pen to end // QuadTo records a quadratic Bézier from the pen to end
// with the control point ctrl, with absolute coordinates. // with the control point ctrl, with absolute coordinates.
func (p *Path) QuadTo(ctrl, to f32.Point) { func (p *Path) QuadTo(ctrl, to f32.Point) {
if ctrl == p.pen && to == p.pen {
return
}
data := ops.WriteMulti(p.ops, scene.CommandSize+4) data := ops.WriteMulti(p.ops, scene.CommandSize+4)
bo := binary.LittleEndian bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour)) bo.PutUint32(data[0:], uint32(p.contour))
+5 -17
View File
@@ -53,7 +53,6 @@ The MacroOp records a list of operations to be executed later:
ops := new(op.Ops) ops := new(op.Ops)
macro := op.Record(ops) macro := op.Record(ops)
// Record operations by adding them. // Record operations by adding them.
op.InvalidateOp{}.Add(ops)
... ...
// End recording. // End recording.
call := macro.Stop() call := macro.Stop()
@@ -96,9 +95,9 @@ type CallOp struct {
end ops.PC end ops.PC
} }
// InvalidateOp requests a redraw at the given time. Use // InvalidateCmd requests a redraw at the given time. Use
// the zero value to request an immediate redraw. // the zero value to request an immediate redraw.
type InvalidateOp struct { type InvalidateCmd struct {
At time.Time At time.Time
} }
@@ -111,7 +110,7 @@ type TransformOp struct {
// TransformStack represents a TransformOp pushed on the transformation stack. // TransformStack represents a TransformOp pushed on the transformation stack.
type TransformStack struct { type TransformStack struct {
id ops.StackID id ops.StackID
macroID int macroID uint32
ops *ops.Ops ops *ops.Ops
} }
@@ -181,19 +180,6 @@ func (c CallOp) Add(o *Ops) {
ops.AddCall(&o.Internal, c.ops, c.start, c.end) ops.AddCall(&o.Internal, c.ops, c.start, c.end)
} }
func (r InvalidateOp) Add(o *Ops) {
data := ops.Write(&o.Internal, ops.TypeRedrawLen)
data[0] = byte(ops.TypeInvalidate)
bo := binary.LittleEndian
// UnixNano cannot represent the zero time.
if t := r.At; !t.IsZero() {
nanos := t.UnixNano()
if nanos > 0 {
bo.PutUint64(data[1:], uint64(nanos))
}
}
}
// Offset converts an offset to a TransformOp. // Offset converts an offset to a TransformOp.
func Offset(off image.Point) TransformOp { func Offset(off image.Point) TransformOp {
offf := f32.Pt(float32(off.X), float32(off.Y)) offf := f32.Pt(float32(off.X), float32(off.Y))
@@ -240,3 +226,5 @@ func (t TransformStack) Pop() {
data := ops.Write(t.ops, ops.TypePopTransformLen) data := ops.Write(t.ops, ops.TypePopTransformLen)
data[0] = byte(ops.TypePopTransform) data[0] = byte(ops.TypePopTransform)
} }
func (InvalidateCmd) ImplementsCommand() {}
+49
View File
@@ -15,8 +15,20 @@ import (
"gioui.org/op/clip" "gioui.org/op/clip"
) )
// ImageFilter is the scaling filter for images.
type ImageFilter byte
const (
// FilterLinear uses linear interpolation for scaling.
FilterLinear ImageFilter = iota
// FilterNearest uses nearest neighbor interpolation for scaling.
FilterNearest
)
// ImageOp sets the brush to an image. // ImageOp sets the brush to an image.
type ImageOp struct { type ImageOp struct {
Filter ImageFilter
uniform bool uniform bool
color color.NRGBA color color.NRGBA
src *image.RGBA src *image.RGBA
@@ -44,6 +56,14 @@ type LinearGradientOp struct {
type PaintOp struct { type PaintOp struct {
} }
// OpacityStack represents an opacity applied to all painting operations
// until Pop is called.
type OpacityStack struct {
id ops.StackID
macroID uint32
ops *ops.Ops
}
// NewImageOp creates an ImageOp backed by src. // NewImageOp creates an ImageOp backed by src.
// //
// NewImageOp assumes the backing image is immutable, and may cache a // NewImageOp assumes the backing image is immutable, and may cache a
@@ -95,6 +115,7 @@ func (i ImageOp) Add(o *op.Ops) {
} }
data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle) data := ops.Write2(&o.Internal, ops.TypeImageLen, i.src, i.handle)
data[0] = byte(ops.TypeImage) data[0] = byte(ops.TypeImage)
data[1] = byte(i.Filter)
} }
func (c ColorOp) Add(o *op.Ops) { func (c ColorOp) Add(o *op.Ops) {
@@ -145,3 +166,31 @@ func Fill(ops *op.Ops, c color.NRGBA) {
ColorOp{Color: c}.Add(ops) ColorOp{Color: c}.Add(ops)
PaintOp{}.Add(ops) PaintOp{}.Add(ops)
} }
// PushOpacity creates a drawing layer with an opacity in the range [0;1].
// The layer includes every subsequent drawing operation until [OpacityStack.Pop]
// is called.
//
// The layer is drawn in two steps. First, the layer operations are
// drawn to a separate image. Then, the image is blended on top of
// the frame, with the opacity used as the blending factor.
func PushOpacity(o *op.Ops, opacity float32) OpacityStack {
if opacity > 1 {
opacity = 1
}
if opacity < 0 {
opacity = 0
}
id, macroID := ops.PushOp(&o.Internal, ops.OpacityStack)
data := ops.Write(&o.Internal, ops.TypePushOpacityLen)
bo := binary.LittleEndian
data[0] = byte(ops.TypePushOpacity)
bo.PutUint32(data[1:], math.Float32bits(opacity))
return OpacityStack{ops: &o.Internal, id: id, macroID: macroID}
}
func (t OpacityStack) Pop() {
ops.PopOp(t.ops, ops.OpacityStack, t.id, t.macroID)
data := ops.Write(t.ops, ops.TypePopOpacityLen)
data[0] = byte(ops.TypePopOpacity)
}
+71 -68
View File
@@ -4,6 +4,7 @@ package text
import ( import (
"bytes" "bytes"
"fmt"
"image" "image"
"io" "io"
"log" "log"
@@ -36,7 +37,8 @@ type document struct {
lines []line lines []line
alignment Alignment alignment Alignment
// alignWidth is the width used when aligning text. // alignWidth is the width used when aligning text.
alignWidth int alignWidth int
unreadRuneCount int
} }
// append adds the lines of other to the end of l and ensures they // append adds the lines of other to the end of l and ensures they
@@ -52,6 +54,7 @@ func (l *document) reset() {
l.lines = l.lines[:0] l.lines = l.lines[:0]
l.alignment = Start l.alignment = Start
l.alignWidth = 0 l.alignWidth = 0
l.unreadRuneCount = 0
} }
func max(a, b int) int { func max(a, b int) int {
@@ -93,6 +96,55 @@ type line struct {
yOffset int yOffset int
} }
// insertTrailingSyntheticNewline adds a synthetic newline to the final logical run of the line
// with the given shaping cluster index.
func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) {
// If there was a newline at the end of this paragraph, insert a synthetic glyph representing it.
finalContentRun := len(l.runs) - 1
// If there was a trailing newline update the rune counts to include
// it on the last line of the paragraph.
l.runeCount += 1
l.runs[finalContentRun].Runes.Count += 1
syntheticGlyph := glyph{
id: 0,
clusterIndex: newLineClusterIdx,
glyphCount: 0,
runeCount: 1,
xAdvance: 0,
yAdvance: 0,
xOffset: 0,
yOffset: 0,
}
// Inset the synthetic newline glyph on the proper end of the run.
if l.runs[finalContentRun].Direction.Progression() == system.FromOrigin {
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, syntheticGlyph)
} else {
// Ensure capacity.
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, glyph{})
copy(l.runs[finalContentRun].Glyphs[1:], l.runs[finalContentRun].Glyphs)
l.runs[finalContentRun].Glyphs[0] = syntheticGlyph
}
}
func (l *line) setTruncatedCount(truncatedCount int) {
// If we've truncated the text with a truncator, adjust the rune counts within the
// truncator to make it represent the truncated text.
finalRunIdx := len(l.runs) - 1
l.runs[finalRunIdx].truncator = true
finalGlyphIdx := len(l.runs[finalRunIdx].Glyphs) - 1
// The run represents all of the truncated text.
l.runs[finalRunIdx].Runes.Count = truncatedCount
// Only the final glyph represents any runes, and it represents all truncated text.
for i := range l.runs[finalRunIdx].Glyphs {
if i == finalGlyphIdx {
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncatedCount
} else {
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
}
}
}
// Range describes the position and quantity of a range of text elements // Range describes the position and quantity of a range of text elements
// within a larger slice. The unit is usually runes of unicode data or // within a larger slice. The unit is usually runes of unicode data or
// glyphs of shaped font data. // glyphs of shaped font data.
@@ -225,7 +277,8 @@ func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
// It returns whether the face is now available for use. FontFaces are prioritized // It returns whether the face is now available for use. FontFaces are prioritized
// in the order in which they are loaded, with the first face being the default. // in the order in which they are loaded, with the first face being the default.
func (s *shaperImpl) Load(f FontFace) { func (s *shaperImpl) Load(f FontFace) {
s.fontMap.AddFace(f.Face.Face(), opentype.FontToDescription(f.Font)) desc := opentype.FontToDescription(f.Font)
s.fontMap.AddFace(f.Face.Face(), fontscan.Location{File: fmt.Sprint(desc)}, desc)
s.addFace(f.Face.Face(), f.Font) s.addFace(f.Face.Face(), f.Font)
} }
@@ -233,6 +286,7 @@ func (s *shaperImpl) addFace(f font.Face, md giofont.Font) {
if _, ok := s.faceToIndex[f.Font]; ok { if _, ok := s.faceToIndex[f.Font]; ok {
return return
} }
s.logger.Printf("loaded face %s(style:%s, weight:%d)", md.Typeface, md.Style, md.Weight)
idx := len(s.faces) idx := len(s.faces)
s.faceToIndex[f.Font] = idx s.faceToIndex[f.Font] = idx
s.faces = append(s.faces, f) s.faces = append(s.faces, f)
@@ -528,35 +582,21 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
if hasNewline { if hasNewline {
txt = txt[:len(txt)-1] txt = txt[:len(txt)-1]
} }
truncatedNewline := false if params.MaxLines != 0 && hasNewline {
if hasNewline && len(txt) == 0 { // If we might end up truncating a trailing newline, we must insert the truncator symbol
params.forceTruncate = false // on the final line (if we hit the limit).
// If we only have a newline, shape a space to get line metrics. params.forceTruncate = true
ls, truncated = s.shapeAndWrapText(params, []rune{' '})
if truncated > 0 {
// Our space was truncated. Since our space didn't exist in any meaningful
// capacity, ensure the truncated count is zeroed out.
truncated = 0
truncatedNewline = true
} else {
// We shaped a space to get proper line metrics, but we need to drop
// the rune/glyph info since it isn't actually part of the text.
ls[0][0].Glyphs = ls[0][0].Glyphs[:0]
ls[0][0].Advance = 0
ls[0][0].Runes.Count = 0
}
} else {
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
} }
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
didTruncate := truncated > 0 || truncatedNewline || (params.forceTruncate && params.MaxLines == len(ls)) hasTruncator := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
if hasTruncator && hasNewline {
if didTruncate && hasNewline { // We have a truncator at the end of the line, so the newline is logically
// We've truncated the newline, since it was at the end and we've truncated some amount of runes // truncated as well.
// before it.
truncated++ truncated++
hasNewline = false hasNewline = false
} }
// Convert to Lines. // Convert to Lines.
textLines := make([]line, len(ls)) textLines := make([]line, len(ls))
maxHeight := fixed.Int26_6(0) maxHeight := fixed.Int26_6(0)
@@ -565,49 +605,12 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
if otLine.lineHeight > maxHeight { if otLine.lineHeight > maxHeight {
maxHeight = otLine.lineHeight maxHeight = otLine.lineHeight
} }
isFinalLine := i == len(ls)-1 if isFinalLine := i == len(ls)-1; isFinalLine {
if isFinalLine && hasNewline { if hasNewline {
// If there was a trailing newline update the rune counts to include otLine.insertTrailingSyntheticNewline(len(txt))
// it on the last line of the paragraph.
finalRunIdx := len(otLine.runs) - 1
otLine.runeCount += 1
otLine.runs[finalRunIdx].Runes.Count += 1
syntheticGlyph := glyph{
id: 0,
clusterIndex: len(txt),
glyphCount: 0,
runeCount: 1,
xAdvance: 0,
yAdvance: 0,
xOffset: 0,
yOffset: 0,
} }
// Inset the synthetic newline glyph on the proper end of the run. if hasTruncator {
if otLine.runs[finalRunIdx].Direction.Progression() == system.FromOrigin { otLine.setTruncatedCount(truncated)
otLine.runs[finalRunIdx].Glyphs = append(otLine.runs[finalRunIdx].Glyphs, syntheticGlyph)
} else {
// Ensure capacity.
otLine.runs[finalRunIdx].Glyphs = append(otLine.runs[finalRunIdx].Glyphs, glyph{})
copy(otLine.runs[finalRunIdx].Glyphs[1:], otLine.runs[finalRunIdx].Glyphs)
otLine.runs[finalRunIdx].Glyphs[0] = syntheticGlyph
}
}
if isFinalLine && didTruncate {
// If we've truncated the text with a truncator, adjust the rune counts within the
// truncator to make it represent the truncated text.
finalRunIdx := len(otLine.runs) - 1
otLine.runs[finalRunIdx].truncator = true
finalGlyphIdx := len(otLine.runs[finalRunIdx].Glyphs) - 1
// The run represents all of the truncated text.
otLine.runs[finalRunIdx].Runes.Count = truncated
// Only the final glyph represents any runes, and it represents all truncated text.
for i := range otLine.runs[finalRunIdx].Glyphs {
if i == finalGlyphIdx {
otLine.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncated
} else {
otLine.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
}
} }
} }
textLines[i] = otLine textLines[i] = otLine
+19 -24
View File
@@ -3,9 +3,8 @@
package text package text
import ( import (
"encoding/binary"
"hash/maphash"
"image" "image"
"sync/atomic"
giofont "gioui.org/font" giofont "gioui.org/font"
"gioui.org/io/system" "gioui.org/io/system"
@@ -88,32 +87,32 @@ type glyphValue[V any] struct {
} }
type glyphLRU[V any] struct { type glyphLRU[V any] struct {
seed maphash.Seed seed uint64
cache lru[uint64, glyphValue[V]] cache lru[uint64, glyphValue[V]]
} }
var seed uint32
// hashGlyphs computes a hash key based on the ID and X offset of // hashGlyphs computes a hash key based on the ID and X offset of
// every glyph in the slice. // every glyph in the slice.
func (c *glyphLRU[V]) hashGlyphs(gs []Glyph) uint64 { func (c *glyphLRU[V]) hashGlyphs(gs []Glyph) uint64 {
if c.seed == (maphash.Seed{}) { if c.seed == 0 {
c.seed = maphash.MakeSeed() c.seed = uint64(atomic.AddUint32(&seed, 3900798947))
} }
var h maphash.Hash if len(gs) == 0 {
h.SetSeed(c.seed) return 0
var b [8]byte
firstX := fixed.Int26_6(0)
for i, g := range gs {
if i == 0 {
firstX = g.X
}
// Cache glyph X offsets relative to the first glyph.
binary.LittleEndian.PutUint32(b[:4], uint32(g.X-firstX))
h.Write(b[:4])
binary.LittleEndian.PutUint64(b[:], uint64(g.ID))
h.Write(b[:])
} }
sum := h.Sum64()
return sum h := c.seed
firstX := gs[0].X
for _, g := range gs {
h += uint64(g.X - firstX)
h *= 6585573582091643
h += uint64(g.ID)
h *= 3650802748644053
}
return h
} }
func (c *glyphLRU[V]) Get(key uint64, gs []Glyph) (V, bool) { func (c *glyphLRU[V]) Get(key uint64, gs []Glyph) (V, bool) {
@@ -164,10 +163,6 @@ type layoutKey struct {
lineHeightScale float32 lineHeightScale float32
} }
type pathKey struct {
gidHash uint64
}
const maxSize = 1000 const maxSize = 1000
func gidsEqual(a []glyphInfo, glyphs []Glyph) bool { func gidsEqual(a []glyphInfo, glyphs []Glyph) bool {
+11 -9
View File
@@ -121,7 +121,7 @@ type Glyph struct {
// belongs to. If Flags does not contain FlagClusterBreak, this value will // belongs to. If Flags does not contain FlagClusterBreak, this value will
// always be zero. The final glyph in the cluster contains the runes count // always be zero. The final glyph in the cluster contains the runes count
// for the entire cluster. // for the entire cluster.
Runes int Runes uint16
// Flags encode special properties of this glyph. // Flags encode special properties of this glyph.
Flags Flags Flags Flags
} }
@@ -245,8 +245,11 @@ func WithCollection(collection []FontFace) ShaperOption {
} }
} }
// NewShaper constructs a shaper with the provided collection of font faces // NewShaper constructs a shaper with the provided options.
// available. //
// NewShaper must be called after [app.NewWindow], unless the [NoSystemFonts]
// option is specified. This is an unfortunate restriction caused by some platforms
// such as Android.
func NewShaper(options ...ShaperOption) *Shaper { func NewShaper(options ...ShaperOption) *Shaper {
l := &Shaper{} l := &Shaper{}
for _, opt := range options { for _, opt := range options {
@@ -351,11 +354,7 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
unreadRunes++ unreadRunes++
} }
} }
lastLineIdx := len(lines.lines) - 1 l.txt.unreadRuneCount = unreadRunes
lastRunIdx := len(lines.lines[lastLineIdx].runs) - 1
lastGlyphIdx := len(lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs) - 1
lines.lines[lastLineIdx].runs[lastRunIdx].Runes.Count += unreadRunes
lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs[lastGlyphIdx].runeCount += unreadRunes
} }
} }
l.txt.append(lines) l.txt.append(lines)
@@ -470,7 +469,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
Ascent: line.ascent, Ascent: line.ascent,
Descent: line.descent, Descent: line.descent,
Advance: g.xAdvance, Advance: g.xAdvance,
Runes: g.runeCount, Runes: uint16(g.runeCount),
Offset: fixed.Point26_6{ Offset: fixed.Point26_6{
X: g.xOffset, X: g.xOffset,
Y: g.yOffset, Y: g.yOffset,
@@ -505,6 +504,9 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
} }
if endOfCluster { if endOfCluster {
glyph.Flags |= FlagClusterBreak glyph.Flags |= FlagClusterBreak
if run.truncator {
glyph.Runes += uint16(l.txt.unreadRuneCount)
}
} else { } else {
glyph.Runes = 0 glyph.Runes = 0
} }
+41 -21
View File
@@ -51,9 +51,9 @@ func TestWrappingTruncation(t *testing.T) {
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() { for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
glyphs = append(glyphs, g) glyphs = append(glyphs, g)
if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 { if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 {
truncatedRunes += g.Runes truncatedRunes += int(g.Runes)
} else { } else {
untruncatedRunes += g.Runes untruncatedRunes += int(g.Runes)
} }
if g.Flags&FlagLineBreak != 0 { if g.Flags&FlagLineBreak != 0 {
lineCount++ lineCount++
@@ -117,9 +117,9 @@ func TestWrappingForcedTruncation(t *testing.T) {
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() { for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
glyphs = append(glyphs, g) glyphs = append(glyphs, g)
if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 { if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 {
truncatedRunes += g.Runes truncatedRunes += int(g.Runes)
} else { } else {
untruncatedRunes += g.Runes untruncatedRunes += int(g.Runes)
} }
if g.Flags&FlagLineBreak != 0 { if g.Flags&FlagLineBreak != 0 {
lineCount++ lineCount++
@@ -154,9 +154,11 @@ func TestWrappingForcedTruncation(t *testing.T) {
// consistently and does not create spurious lines of text. // consistently and does not create spurious lines of text.
func TestShapingNewlineHandling(t *testing.T) { func TestShapingNewlineHandling(t *testing.T) {
type testcase struct { type testcase struct {
textInput string textInput string
expectedLines int expectedLines int
expectedGlyphs int expectedGlyphs int
maxLines int
expectedTruncated int
} }
for _, tc := range []testcase{ for _, tc := range []testcase{
{textInput: "a\n", expectedLines: 1, expectedGlyphs: 3}, {textInput: "a\n", expectedLines: 1, expectedGlyphs: 3},
@@ -165,21 +167,41 @@ func TestShapingNewlineHandling(t *testing.T) {
{textInput: "\n", expectedLines: 1, expectedGlyphs: 2}, {textInput: "\n", expectedLines: 1, expectedGlyphs: 2},
{textInput: "\n\n", expectedLines: 2, expectedGlyphs: 3}, {textInput: "\n\n", expectedLines: 2, expectedGlyphs: 3},
{textInput: "\n\n\n", expectedLines: 3, expectedGlyphs: 4}, {textInput: "\n\n\n", expectedLines: 3, expectedGlyphs: 4},
{textInput: "\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 1},
{textInput: "\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 2},
{textInput: "\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 3},
{textInput: "a\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 1},
{textInput: "a\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 2},
{textInput: "a\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 3},
{textInput: "\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 2},
{textInput: "\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 1},
{textInput: "\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 2},
{textInput: "a\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 3},
{textInput: "a\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 1},
{textInput: "a\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 2},
} { } {
t.Run(fmt.Sprintf("%q", tc.textInput), func(t *testing.T) { t.Run(fmt.Sprintf("%q-maxLines%d", tc.textInput, tc.maxLines), func(t *testing.T) {
ltrFace, _ := opentype.Parse(goregular.TTF) ltrFace, _ := opentype.Parse(goregular.TTF)
collection := []FontFace{{Face: ltrFace}} collection := []FontFace{{Face: ltrFace}}
cache := NewShaper(NoSystemFonts(), WithCollection(collection)) cache := NewShaper(NoSystemFonts(), WithCollection(collection))
checkGlyphs := func() { checkGlyphs := func() {
glyphs := []Glyph{} glyphs := []Glyph{}
runes := 0 runes := 0
truncated := 0
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() { for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
glyphs = append(glyphs, g) glyphs = append(glyphs, g)
runes += g.Runes if g.Flags&FlagTruncator == 0 {
runes += int(g.Runes)
} else {
truncated += int(g.Runes)
}
} }
if expected := len([]rune(tc.textInput)); expected != runes { if expected := len([]rune(tc.textInput)) - tc.expectedTruncated; expected != runes {
t.Errorf("expected %d runes, got %d", expected, runes) t.Errorf("expected %d runes, got %d", expected, runes)
} }
if truncated != tc.expectedTruncated {
t.Errorf("expected %d truncated runes, got %d", tc.expectedTruncated, truncated)
}
if len(glyphs) != tc.expectedGlyphs { if len(glyphs) != tc.expectedGlyphs {
t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs)) t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs))
} }
@@ -207,29 +229,27 @@ func TestShapingNewlineHandling(t *testing.T) {
t.Errorf("expected paragraph start glyph to have cursor y") t.Errorf("expected paragraph start glyph to have cursor y")
} }
} }
if count := strings.Count(tc.textInput, "\n"); found != count { if count := strings.Count(tc.textInput, "\n"); found != count && tc.maxLines == 0 {
t.Errorf("expected %d paragraph breaks, found %d", count, found) t.Errorf("expected %d paragraph breaks, found %d", count, found)
} else if tc.maxLines > 0 && found > tc.maxLines {
t.Errorf("expected %d paragraph breaks due to truncation, found %d", tc.maxLines, found)
} }
} }
cache.LayoutString(Parameters{ params := Parameters{
Alignment: Middle, Alignment: Middle,
PxPerEm: fixed.I(10), PxPerEm: fixed.I(10),
MinWidth: 200, MinWidth: 200,
MaxWidth: 200, MaxWidth: 200,
Locale: english, Locale: english,
}, tc.textInput) MaxLines: tc.maxLines,
}
cache.LayoutString(params, tc.textInput)
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines { if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount) t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount)
} }
checkGlyphs() checkGlyphs()
cache.Layout(Parameters{ cache.Layout(params, strings.NewReader(tc.textInput))
Alignment: Middle,
PxPerEm: fixed.I(10),
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, strings.NewReader(tc.textInput))
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines { if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount) t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount)
} }
@@ -551,7 +571,7 @@ func TestShapeStringRuneAccounting(t *testing.T) {
} }
totalRunes := 0 totalRunes := 0
for _, g := range glyphs { for _, g := range glyphs {
totalRunes += g.Runes totalRunes += int(g.Runes)
} }
if inputRunes := len([]rune(tc.input)); totalRunes != inputRunes { if inputRunes := len([]rune(tc.input)); totalRunes != inputRunes {
t.Errorf("input contained %d runes, but glyphs contained %d", inputRunes, totalRunes) t.Errorf("input contained %d runes, but glyphs contained %d", inputRunes, totalRunes)
+10 -18
View File
@@ -11,15 +11,15 @@ type Bool struct {
Value bool Value bool
clk Clickable clk Clickable
changed bool
} }
// Changed reports whether Value has changed since the last // Update the widget state and report whether Value was changed.
// call to Changed. func (b *Bool) Update(gtx layout.Context) bool {
func (b *Bool) Changed() bool { changed := false
changed := b.changed for b.clk.clicked(b, gtx) {
b.changed = false b.Value = !b.Value
changed = true
}
return changed return changed
} }
@@ -33,23 +33,15 @@ func (b *Bool) Pressed() bool {
return b.clk.Pressed() return b.clk.Pressed()
} }
// Focused reports whether b has focus.
func (b *Bool) Focused() bool {
return b.clk.Focused()
}
func (b *Bool) History() []Press { func (b *Bool) History() []Press {
return b.clk.History() return b.clk.History()
} }
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions { b.Update(gtx)
for b.clk.Clicked() { dims := b.clk.layout(b, gtx, func(gtx layout.Context) layout.Dimensions {
b.Value = !b.Value
b.changed = true
}
semantic.SelectedOp(b.Value).Add(gtx.Ops) semantic.SelectedOp(b.Value).Add(gtx.Ops)
semantic.DisabledOp(gtx.Queue == nil).Add(gtx.Ops) semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
return w(gtx) return w(gtx)
}) })
return dims return dims

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