153 Commits

Author SHA1 Message Date
Elias Naur 3879921b80 app: [API] remove Main
All platforms already allow the omission of the call to Main and running
Windows on the main goroutine. This change just gets rid of Main, and
documents the special requirement on Window.Event.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:53:33 +00:00
Elias Naur b1f84da679 app: [macOS] implement custom event dispatching
To get rid of app.Main, we need to control the main thread. The macOS
[NSApp run] must be called on the main goroutine and never yields control.

Implement the escape hatch which is calling [NSApp stop] to force
[NSApp run] to return and allow us to fetch and dispatch events one at a
time.

This change is separate from the larger change removing app.Main to ease
bisecting.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:53:12 +00:00
Elias Naur 43024fcca2 app: [macOS] implement non-blocking window resizing
Despite the option to control the main thread event loop, some gestures
still block the event loop until completion. This change disables the
blocking resize gestures and implements a non-blocking replacement.

A complete replacement is left for future work, or the implementation of
https://github.com/golang/go/issues/64755.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:52:04 +00:00
Elias Naur 9fe8b684e2 app: introduce Config.Focused that tracks the window focus state
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:52:04 +00:00
Elias Naur 2a18a0c135 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-02-08 18:52:04 +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
129 changed files with 8206 additions and 7006 deletions
+16 -6
View File
@@ -8,16 +8,21 @@ packages:
- libxml2-dev
- libssl-dev
- libz-dev
- llvm-dev # for cctools
- uuid-dev ## for cctools
- llvm-dev # cctools
- uuid-dev # cctools
- ninja-build # cctools
- systemtap-sdt-dev # cctools
- libbsd-dev # cctools
- linux-libc-dev # cctools
- libplist-utils # for gogio
sources:
- https://git.sr.ht/~eliasnaur/applesdks
- https://git.sr.ht/~eliasnaur/gio
- https://git.sr.ht/~eliasnaur/giouiorg
- https://github.com/tpoechtrager/cctools-port.git
- https://github.com/tpoechtrager/apple-libtapi.git
- https://github.com/mackyle/xar.git
- https://github.com/tpoechtrager/cctools-port
- https://github.com/tpoechtrager/apple-libtapi
- https://github.com/tpoechtrager/apple-libdispatch
- https://github.com/mackyle/xar
environment:
APPLE_TOOLCHAIN_ROOT: /home/build/appletools
PATH: /home/build/sdk/go/bin:/home/build/go/bin:/usr/bin
@@ -42,6 +47,11 @@ tasks:
- install_appletoolchain: |
cd giouiorg
go build -o $APPLE_TOOLCHAIN_ROOT/tools ./cmd/appletoolchain
- build_libdispatch: |
cd apple-libdispatch
cmake -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_INSTALL_PREFIX=$APPLE_TOOLCHAIN_ROOT/libdispatch .
ninja
ninja install
- build_xar: |
cd xar/xar
ac_cv_lib_crypto_OpenSSL_add_all_ciphers=yes CC=clang ./autogen.sh --prefix=/usr
@@ -53,7 +63,7 @@ tasks:
./install.sh
- build_cctools: |
cd cctools-port/cctools
./configure --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --target=x86_64-apple-darwin19
./configure --target=x86_64-apple-darwin19 --prefix $APPLE_TOOLCHAIN_ROOT/toolchain --with-libtapi=$APPLE_TOOLCHAIN_ROOT/libtapi --with-libdispatch=$APPLE_TOOLCHAIN_ROOT/libdispatch --with-libblocksruntime=$APPLE_TOOLCHAIN_ROOT/libdispatch
make install
- test_macos: |
cd gio
-5
View File
@@ -67,11 +67,6 @@ tasks:
CGO_ENABLED=1 GOARCH=386 go test ./...
GOOS=windows go test -exec=wine ./...
GOOS=js GOARCH=wasm go build -o /dev/null ./...
- install_chrome: |
curl -s https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
sudo sh -c 'echo "deb [arch=amd64] https://dl-ssl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
sudo apt-get -qq update
sudo apt-get -qq install -y google-chrome-stable
- install_jdk8: |
curl -so jdk.deb "https://cdn.azul.com/zulu/bin/zulu8.42.0.21-ca-jdk8.0.232-linux_amd64.deb"
sudo apt-get -qq install -y -f ./jdk.deb
+1 -1
View File
@@ -65,8 +65,8 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
private final InputMethodManager imm;
private final float scrollXScale;
private final float scrollYScale;
private final AccessibilityManager accessManager;
private int keyboardHint;
private AccessibilityManager accessManager;
private long nhandle;
+87 -19
View File
@@ -3,9 +3,16 @@
package app
import (
"image"
"os"
"path/filepath"
"strings"
"time"
"gioui.org/io/input"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/unit"
)
// extraArgs contains extra arguments to append to
@@ -20,23 +27,84 @@ var extraArgs string
// On Android ID is the package property of AndroidManifest.xml,
// on iOS ID is the CFBundleIdentifier of the app Info.plist,
// on Wayland it is the toplevel app_id,
// on X11 it is the X11 XClassHint
// on X11 it is the X11 XClassHint.
//
// ID is set by the gogio tool or manually with the -X linker flag. For example,
// ID is set by the [gioui.org/cmd/gogio] tool or manually with the -X linker flag. For example,
//
// go build -ldflags="-X 'gioui.org/app.ID=org.gioui.example.Kitchen'" .
//
// Note that ID is treated as a constant, and that changing it at runtime
// is not supported. Default value of ID is filepath.Base(os.Args[0]).
// is not supported. The default value of ID is filepath.Base(os.Args[0]).
var ID = ""
func init() {
if extraArgs != "" {
args := strings.Split(extraArgs, "|")
os.Args = append(os.Args, args...)
// A FrameEvent requests a new frame in the form of a list of
// operations that describes the window content.
type FrameEvent struct {
// Now is the current animation. Use Now instead of time.Now to
// synchronize animation and to avoid the time.Now call overhead.
Now time.Time
// Metric converts device independent dp and sp to device pixels.
Metric unit.Metric
// Size is the dimensions of the window.
Size image.Point
// Insets represent the space occupied by system decorations and controls.
Insets Insets
// Frame completes the FrameEvent by drawing the graphical operations
// from ops into the window.
Frame func(frame *op.Ops)
// Source is the interface between the window and widgets.
Source input.Source
}
// ViewEvent provides handles to the underlying window objects for the
// current display protocol.
type ViewEvent interface {
implementsViewEvent()
ImplementsEvent()
}
// 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),
}
}
@@ -52,14 +120,14 @@ func DataDir() (string, error) {
return dataDir()
}
// Main must be called last from the program main function.
// On most platforms Main blocks forever, for Android and
// iOS it returns immediately to give control of the main
// thread back to the system.
//
// Calling Main is necessary because some operating systems
// require control of the main thread of the program for
// running windows.
func Main() {
osMain()
func (FrameEvent) ImplementsEvent() {}
func init() {
if extraArgs != "" {
args := strings.Split(extraArgs, "|")
os.Args = append(os.Args, args...)
}
if ID == "" {
ID = filepath.Base(os.Args[0])
}
}
+5 -5
View File
@@ -60,10 +60,10 @@ func (c *d3d11Context) RenderTarget() (gpu.RenderTarget, error) {
}
func (c *d3d11Context) Present() error {
err := c.swchain.Present(1, 0)
if err == nil {
return nil
}
return wrapErr(c.swchain.Present(1, 0))
}
func wrapErr(err error) error {
if err, ok := err.(d3d11.ErrorCode); ok {
switch err.Code {
case d3d11.DXGI_STATUS_OCCLUDED:
@@ -84,7 +84,7 @@ func (c *d3d11Context) Refresh() error {
}
c.releaseFBO()
if err := c.swchain.ResizeBuffers(0, 0, 0, d3d11.DXGI_FORMAT_UNKNOWN, 0); err != nil {
return err
return wrapErr(err)
}
c.width = width
c.height = height
+16 -38
View File
@@ -8,21 +8,20 @@ See https://gioui.org for instructions to set up and run Gio programs.
# Windows
Create a new Window by calling NewWindow. On mobile platforms or when Gio
is embedded in another project, NewWindow merely connects with a previously
created window.
A Window is run by calling its Event method in a loop. The first time a
method on Window is called, a new GUI window is created and shown. On mobile
platforms or when Gio is embedded in another project, Window merely connects
with a previously created GUI window.
A Window is run by receiving events from its Events channel. The most
important event is FrameEvent that prompts an update of the window
contents and state.
The most important event is [FrameEvent] that prompts an update of the window
contents.
For example:
import "gioui.org/unit"
w := app.NewWindow()
for e := range w.Events() {
if e, ok := e.(system.FrameEvent); ok {
w := new(app.Window)
for {
e := w.Event()
if e, ok := e.(app.FrameEvent); ok {
ops.Reset()
// Add operations to ops.
...
@@ -32,35 +31,14 @@ For example:
}
A program must keep receiving events from the event channel until
DestroyEvent is received.
[DestroyEvent] is received.
# Main
# Main Thread
The Main function must be called from a program's main function, to hand over
control of the main thread to operating systems that need it.
Because Main is also blocking on some platforms, the event loop of a Window must run in a goroutine.
For example, to display a blank but otherwise functional window:
package main
import "gioui.org/app"
func main() {
go func() {
w := app.NewWindow()
for range w.Events() {
}
}()
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.
Some GUI platform need access to the main thread of the program. To avoid a
deadlock on such platforms, at least one Window must have its Event method
called by the main goroutine. It doesn't have to be any particular Window;
even a destroyed Window suffices.
# Permissions
+11 -1
View File
@@ -69,7 +69,17 @@ func (c *wlContext) Refresh() error {
}
c.eglWin = eglWin
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
return c.Context.CreateSurface(eglSurf, width, height)
if err := c.Context.CreateSurface(eglSurf, width, height); 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 {
+1 -1
View File
@@ -46,8 +46,8 @@ func (c *x11Context) Refresh() error {
if err := c.Context.MakeCurrent(); err != nil {
return err
}
defer c.Context.ReleaseCurrent()
c.Context.EnableVSync(true)
c.Context.ReleaseCurrent()
return nil
}
+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/gofont"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/router"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
@@ -31,10 +31,10 @@ func FuzzIME(f *testing.F) {
f.Fuzz(func(t *testing.T, cmds []byte) {
cache := text.NewShaper(text.WithCollection(gofont.Collection()))
e := new(widget.Editor)
e.Focus()
var r router.Router
gtx := layout.Context{Ops: new(op.Ops), Queue: &r}
var r input.Router
gtx := layout.Context{Ops: new(op.Ops), Source: r.Source()}
gtx.Execute(key.FocusCmd{Tag: e})
// Layout once to register focus.
e.Layout(gtx, cache, font.Font{}, unit.Sp(10), op.CallOp{}, op.CallOp{})
r.Frame(gtx.Ops)
-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"
+41
View File
@@ -47,6 +47,13 @@ type WndClassEx struct {
HIconSm syscall.Handle
}
type Margins struct {
CxLeftWidth int32
CxRightWidth int32
CyTopHeight int32
CyBottomHeight int32
}
type Msg struct {
Hwnd syscall.Handle
Message uint32
@@ -69,6 +76,21 @@ type MinMaxInfo struct {
PtMaxTrackSize Point
}
type NCCalcSizeParams struct {
Rgrc [3]Rect
LpPos *WindowPos
}
type WindowPos struct {
HWND syscall.Handle
HWNDInsertAfter syscall.Handle
x int32
y int32
cx int32
cy int32
flags uint32
}
type WindowPlacement struct {
length uint32
flags uint32
@@ -245,6 +267,7 @@ const (
WM_MOUSEHWHEEL = 0x020E
WM_NCACTIVATE = 0x0086
WM_NCHITTEST = 0x0084
WM_NCCALCSIZE = 0x0083
WM_PAINT = 0x000F
WM_QUIT = 0x0012
WM_SETCURSOR = 0x0020
@@ -323,6 +346,7 @@ var (
_DispatchMessage = user32.NewProc("DispatchMessageW")
_EmptyClipboard = user32.NewProc("EmptyClipboard")
_GetWindowRect = user32.NewProc("GetWindowRect")
_GetClientRect = user32.NewProc("GetClientRect")
_GetClipboardData = user32.NewProc("GetClipboardData")
_GetDC = user32.NewProc("GetDC")
_GetDpiForWindow = user32.NewProc("GetDpiForWindow")
@@ -379,6 +403,9 @@ var (
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
dwmapi = syscall.NewLazySystemDLL("dwmapi")
_DwmExtendFrameIntoClientArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
)
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
@@ -430,6 +457,14 @@ func DispatchMessage(m *Msg) {
_DispatchMessage.Call(uintptr(unsafe.Pointer(m)))
}
func DwmExtendFrameIntoClientArea(hwnd syscall.Handle, margins Margins) error {
r, _, _ := _DwmExtendFrameIntoClientArea.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&margins)))
if r != 0 {
return fmt.Errorf("DwmExtendFrameIntoClientArea: %#x", r)
}
return nil
}
func EmptyClipboard() error {
r, _, err := _EmptyClipboard.Call()
if r == 0 {
@@ -444,6 +479,12 @@ func GetWindowRect(hwnd syscall.Handle) Rect {
return r
}
func GetClientRect(hwnd syscall.Handle) Rect {
var r Rect
_GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r)))
return r
}
func GetClipboardData(format uint32) (syscall.Handle, error) {
r, _, err := _GetClipboardData.Call(uintptr(format))
if r == 0 {
+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))
}
func convertKeysym(s C.xkb_keysym_t) (string, bool) {
func convertKeysym(s C.xkb_keysym_t) (key.Name, bool) {
if 'a' <= s && s <= 'z' {
return string(rune(s - 'a' + 'A')), true
return key.Name(rune(s - 'a' + 'A')), true
}
if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 {
return key.Name(rune(s - C.XKB_KEY_KP_0 + '0')), true
}
if ' ' < s && s <= '~' {
return string(rune(s)), true
return key.Name(rune(s)), true
}
var n string
var n key.Name
switch s {
case C.XKB_KEY_Escape:
n = key.NameEscape
@@ -255,8 +258,6 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
n = key.NameRightArrow
case C.XKB_KEY_Return:
n = key.NameReturn
case C.XKB_KEY_KP_Enter:
n = key.NameEnter
case C.XKB_KEY_Up:
n = key.NameUpArrow
case C.XKB_KEY_Down:
@@ -297,9 +298,9 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
n = key.NameF11
case C.XKB_KEY_F12:
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
case 0x20, C.XKB_KEY_KP_Space:
case 0x20:
n = key.NameSpace
case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R:
n = key.NameCtrl
@@ -309,6 +310,64 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
n = key.NameAlt
case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R:
n = key.NameSuper
case C.XKB_KEY_KP_Space:
n = key.NameSpace
case C.XKB_KEY_KP_Tab:
n = key.NameTab
case C.XKB_KEY_KP_Enter:
n = key.NameEnter
case C.XKB_KEY_KP_F1:
n = key.NameF1
case C.XKB_KEY_KP_F2:
n = key.NameF2
case C.XKB_KEY_KP_F3:
n = key.NameF3
case C.XKB_KEY_KP_F4:
n = key.NameF4
case C.XKB_KEY_KP_Home:
n = key.NameHome
case C.XKB_KEY_KP_Left:
n = key.NameLeftArrow
case C.XKB_KEY_KP_Up:
n = key.NameUpArrow
case C.XKB_KEY_KP_Right:
n = key.NameRightArrow
case C.XKB_KEY_KP_Down:
n = key.NameDownArrow
case C.XKB_KEY_KP_Prior:
// not supported
return "", false
case C.XKB_KEY_KP_Next:
// not supported
return "", false
case C.XKB_KEY_KP_End:
n = key.NameEnd
case C.XKB_KEY_KP_Begin:
n = key.NameHome
case C.XKB_KEY_KP_Insert:
// not supported
return "", false
case C.XKB_KEY_KP_Delete:
n = key.NameDeleteForward
case C.XKB_KEY_KP_Multiply:
n = "*"
case C.XKB_KEY_KP_Add:
n = "+"
case C.XKB_KEY_KP_Separator:
// not supported
return "", false
case C.XKB_KEY_KP_Subtract:
n = "-"
case C.XKB_KEY_KP_Decimal:
// TODO(dh): does a German keyboard layout also translate the numpad key to XKB_KEY_KP_DECIMAL? Because in
// German, the decimal is a comma, not a period.
n = "."
case C.XKB_KEY_KP_Divide:
n = "/"
case C.XKB_KEY_KP_Equal:
n = "="
default:
return "", false
}
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
package log
package app
/*
#cgo LDFLAGS: -llog
@@ -22,7 +22,7 @@ import (
// 1024 is the truncation limit from android/log.h, plus a \n.
const logLineLimit = 1024
var logTag = C.CString(appID)
var logTag = C.CString(ID)
func init() {
// Android's logcat already includes timestamps.
@@ -3,7 +3,7 @@
//go:build darwin && ios
// +build darwin,ios
package log
package app
/*
#cgo CFLAGS: -Werror -fmodules -fobjc-arc -x objective-c
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
package log
package app
import (
"log"
+2 -1
View File
@@ -60,8 +60,9 @@ static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) {
id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
[cmdBuffer presentDrawable:drawable];
[cmdBuffer commit];
[cmdBuffer waitUntilScheduled];
[drawable present];
}
}
+4 -1
View File
@@ -21,7 +21,10 @@ Class gio_layerClass(void) {
static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
@autoreleasepool {
UIView *view = (__bridge UIView *)viewRef;
return CFBridgingRetain(view.layer);
CAMetalLayer *l = (CAMetalLayer *)view.layer;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = YES;
return CFBridgingRetain(l);
}
}
+5 -1
View File
@@ -14,7 +14,11 @@ package app
CALayer *gio_layerFactory(void) {
@autoreleasepool {
return [CAMetalLayer layer];
CAMetalLayer *l = [CAMetalLayer layer];
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = YES;
return l;
}
}
+171 -21
View File
@@ -7,7 +7,9 @@ import (
"image"
"image/color"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/op"
"gioui.org/gpu"
"gioui.org/io/pointer"
@@ -43,6 +45,8 @@ type Config struct {
CustomRenderer bool
// Decorated reports whether window decorations are provided automatically.
Decorated bool
// Focused reports whether has the keyboard focus.
Focused bool
// decoHeight is the height of the fallback decoration for platforms such
// as Wayland that may need fallback client-side decorations.
decoHeight unit.Dp
@@ -131,8 +135,30 @@ func (o Orientation) String() string {
return ""
}
// eventLoop implements the functionality required for drivers where
// window event loops must run on a separate thread.
type eventLoop struct {
win *callbacks
// wakeup is the callback to wake up the event loop.
wakeup func()
// driverFuncs is a channel of functions to run the next
// time the window loop waits for events.
driverFuncs chan func()
// invalidates is notified when an invalidate is requested by the client.
invalidates chan struct{}
// immediateInvalidates is an optimistic invalidates that doesn't require a wakeup.
immediateInvalidates chan struct{}
// events is where the platform backend delivers events bound for the
// user program.
events chan event.Event
frames chan *op.Ops
frameAck chan struct{}
// delivering avoids re-entrant event delivery.
delivering bool
}
type frameEvent struct {
system.FrameEvent
FrameEvent
Sync bool
}
@@ -147,9 +173,19 @@ type context interface {
Unlock()
}
// Driver is the interface for the platform implementation
// basicDriver is the subset of [driver] that may be called even after
// a window is destroyed.
type basicDriver interface {
// Event blocks until an even is available and returns it.
Event() event.Event
// Invalidate requests a FrameEvent.
Invalidate()
}
// driver is the interface for the platform implementation
// of a window.
type driver interface {
basicDriver
// SetAnimating sets the animation flag. When the window is animating,
// FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool)
@@ -160,23 +196,29 @@ type driver interface {
// ReadClipboard requests the clipboard content.
ReadClipboard()
// WriteClipboard requests a clipboard write.
WriteClipboard(s string)
WriteClipboard(mime string, s []byte)
// Configure the window.
Configure([]Option)
// SetCursor updates the current cursor to name.
SetCursor(cursor pointer.Cursor)
// Wakeup wakes up the event loop and sends a WakeupEvent.
Wakeup()
// Wakeup()
// Perform actions on the window.
Perform(system.Action)
// EditorStateChanged notifies the driver that the editor state changed.
EditorStateChanged(old, new editorState)
// Run a function on the window thread.
Run(f func())
// Frame receives a frame.
Frame(frame *op.Ops)
// ProcessEvent processes an event.
ProcessEvent(e event.Event)
}
type windowRendezvous struct {
in chan windowAndConfig
out chan windowAndConfig
errs chan error
in chan windowAndConfig
out chan windowAndConfig
windows chan struct{}
}
type windowAndConfig struct {
@@ -186,32 +228,137 @@ type windowAndConfig struct {
func newWindowRendezvous() *windowRendezvous {
wr := &windowRendezvous{
in: make(chan windowAndConfig),
out: make(chan windowAndConfig),
errs: make(chan error),
in: make(chan windowAndConfig),
out: make(chan windowAndConfig),
windows: make(chan struct{}),
}
go func() {
var main windowAndConfig
in := wr.in
var window windowAndConfig
var out chan windowAndConfig
for {
select {
case w := <-wr.in:
var err error
if main.window != nil {
err = errors.New("multiple windows are not supported")
}
wr.errs <- err
main = w
case w := <-in:
window = w
out = wr.out
case out <- main:
case out <- window:
}
}
}()
return wr
}
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
func newEventLoop(w *callbacks, wakeup func()) *eventLoop {
return &eventLoop{
win: w,
wakeup: wakeup,
events: make(chan event.Event),
invalidates: make(chan struct{}, 1),
immediateInvalidates: make(chan struct{}),
frames: make(chan *op.Ops),
frameAck: make(chan struct{}),
driverFuncs: make(chan func(), 1),
}
}
// Frame receives a frame and waits for its processing. It is called by
// the client goroutine.
func (e *eventLoop) Frame(frame *op.Ops) {
e.frames <- frame
<-e.frameAck
}
// Event returns the next available event. It is called by the client
// goroutine.
func (e *eventLoop) Event() event.Event {
for {
evt := <-e.events
// Receiving a flushEvent indicates to the platform backend that
// all previous events have been processed by the user program.
if _, ok := evt.(flushEvent); ok {
continue
}
return evt
}
}
// Invalidate requests invalidation of the window. It is called by the client
// goroutine.
func (e *eventLoop) Invalidate() {
select {
case e.immediateInvalidates <- struct{}{}:
// The event loop was waiting, no need for a wakeup.
case e.invalidates <- struct{}{}:
// The event loop is sleeping, wake it up.
e.wakeup()
default:
// A redraw is pending.
}
}
// Run f in the window loop thread. It is called by the client goroutine.
func (e *eventLoop) Run(f func()) {
e.driverFuncs <- f
e.wakeup()
}
// FlushEvents delivers pending events to the client.
func (e *eventLoop) FlushEvents() {
if e.delivering {
return
}
e.delivering = true
defer func() { e.delivering = false }()
for {
evt, ok := e.win.nextEvent()
if !ok {
break
}
e.deliverEvent(evt)
}
}
func (e *eventLoop) deliverEvent(evt event.Event) {
var frames <-chan *op.Ops
for {
select {
case f := <-e.driverFuncs:
f()
case frame := <-frames:
// The client called FrameEvent.Frame.
frames = nil
e.win.ProcessFrame(frame, e.frameAck)
case e.events <- evt:
switch evt.(type) {
case flushEvent, DestroyEvent:
// DestroyEvents are not flushed.
return
case FrameEvent:
frames = e.frames
}
evt = theFlushEvent
case <-e.invalidates:
e.win.Invalidate()
case <-e.immediateInvalidates:
e.win.Invalidate()
}
}
}
func (e *eventLoop) Wakeup() {
for {
select {
case f := <-e.driverFuncs:
f()
case <-e.invalidates:
e.win.Invalidate()
case <-e.immediateInvalidates:
e.win.Invalidate()
default:
return
}
}
}
func walkActions(actions system.Action, do func(system.Action)) {
for a := system.Action(1); actions != 0; a <<= 1 {
@@ -221,3 +368,6 @@ func walkActions(actions system.Action, do func(system.Action)) {
}
}
}
func (wakeupEvent) ImplementsEvent() {}
func (ConfigEvent) ImplementsEvent() {}
+142 -105
View File
@@ -123,31 +123,36 @@ import (
"fmt"
"image"
"image/color"
"io"
"math"
"os"
"path/filepath"
"runtime"
"runtime/cgo"
"runtime/debug"
"strings"
"sync"
"time"
"unicode/utf16"
"unsafe"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit"
)
type window struct {
callbacks *callbacks
loop *eventLoop
view C.jobject
handle cgo.Handle
@@ -156,18 +161,19 @@ type window struct {
fontScale float32
insets pixelInsets
stage system.Stage
visible bool
started bool
animating bool
win *C.ANativeWindow
config Config
win *C.ANativeWindow
config Config
inputHint key.InputHint
semantic struct {
hoverID router.SemanticID
rootID router.SemanticID
focusID router.SemanticID
diffs []router.SemanticID
hoverID input.SemanticID
rootID input.SemanticID
focusID input.SemanticID
diffs []input.SemanticID
}
}
@@ -199,9 +205,9 @@ type pixelInsets struct {
top, bottom, left, right int
}
// ViewEvent is sent whenever the Window's underlying Android view
// AndroidViewEvent is sent whenever the Window's underlying Android view
// changes.
type ViewEvent struct {
type AndroidViewEvent struct {
// View is a JNI global reference to the android.view.View
// instance backing the Window. The reference is valid until
// the next ViewEvent is received.
@@ -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)
wopts := <-mainWindow.out
var cnf Config
w, ok := windows[wopts.window]
if !ok {
w = &window{
callbacks: wopts.window,
}
w.loop = newEventLoop(w.callbacks, w.wakeup)
w.callbacks.SetDriver(w)
cnf.apply(unit.Metric{}, wopts.options)
windows[wopts.window] = w
} else {
cnf = w.config
}
mainWindow.windows <- struct{}{}
if w.view != 0 {
w.detach(env)
}
w.view = view
w.visible = false
w.handle = cgo.NewHandle(w)
w.callbacks.SetDriver(w)
w.loadConfig(env, class)
w.Configure(wopts.options)
w.SetInputHint(key.HintAny)
w.setStage(system.StagePaused)
w.callbacks.Event(ViewEvent{View: uintptr(view)})
w.setConfig(env, cnf)
w.SetInputHint(w.inputHint)
w.processEvent(AndroidViewEvent{View: uintptr(view)})
return C.jlong(w.handle)
}
@@ -516,7 +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) {
w := cgo.Handle(handle).Value().(*window)
w.started = false
w.setStage(system.StagePaused)
w.visible = false
}
//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) {
w := cgo.Handle(handle).Value().(*window)
w.win = nil
w.setStage(system.StagePaused)
w.visible = false
}
//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) {
w := cgo.Handle(view).Value().(*window)
w.loadConfig(env, class)
if w.stage >= system.StageInactive {
w.draw(env, true)
}
w.draw(env, true)
}
//export Java_org_gioui_GioView_onFrameCallback
@@ -565,10 +575,7 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
if !exist {
return
}
if w.stage < system.StageInactive {
return
}
if w.animating {
if w.visible && w.animating {
w.draw(env, false)
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
func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong) C.jboolean {
w := cgo.Handle(view).Value().(*window)
if w.callbacks.Event(key.Event{Name: key.NameBack}) {
if w.processEvent(key.Event{Name: key.NameBack}) {
return C.JNI_TRUE
}
return C.JNI_FALSE
@@ -586,7 +593,8 @@ func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong)
//export Java_org_gioui_GioView_onFocusChange
func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) {
w := cgo.Handle(view).Value().(*window)
w.callbacks.Event(key.FocusEvent{Focus: focus == C.JNI_TRUE})
w.config.Focused = focus == C.JNI_TRUE
w.processEvent(ConfigEvent{Config: w.config})
}
//export Java_org_gioui_GioView_onWindowInsets
@@ -598,9 +606,7 @@ func Java_org_gioui_GioView_onWindowInsets(env *C.JNIEnv, class C.jclass, view C
left: int(left),
right: int(right),
}
if w.stage >= system.StageInactive {
w.draw(env, true)
}
w.draw(env, true)
}
//export Java_org_gioui_GioView_initializeAccessibilityNodeInfo
@@ -661,7 +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 {
err := callVoidMethod(env, info, android.accessibilityNodeInfo.addChild, jvalue(w.view), jvalue(w.virtualIDFor(ch.ID)))
if err != nil {
@@ -704,7 +738,7 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNod
panic(err)
}
}
if d.Gestures&router.ClickGesture != 0 {
if d.Gestures&input.ClickGesture != 0 {
addAction(ACTION_CLICK)
}
clsName := android.strings.androidViewView
@@ -749,25 +783,23 @@ func (w *window) initAccessibilityNodeInfo(env *C.JNIEnv, sem router.SemanticNod
return nil
}
func (w *window) virtualIDFor(id router.SemanticID) C.jint {
// TODO: Android virtual IDs are 32-bit Java integers, but childID is a int64.
func (w *window) virtualIDFor(id input.SemanticID) C.jint {
if id == w.semantic.rootID {
return HOST_VIEW_ID
}
return C.jint(id)
}
func (w *window) semIDFor(virtID C.jint) router.SemanticID {
func (w *window) semIDFor(virtID C.jint) input.SemanticID {
if virtID == HOST_VIEW_ID {
return w.semantic.rootID
}
return router.SemanticID(virtID)
return input.SemanticID(virtID)
}
func (w *window) detach(env *C.JNIEnv) {
callVoidMethod(env, w.view, gioView.unregister)
w.callbacks.Event(ViewEvent{})
w.callbacks.SetDriver(nil)
w.processEvent(AndroidViewEvent{})
w.handle.Delete()
C.jni_DeleteGlobalRef(env, w.view)
w.view = 0
@@ -778,18 +810,10 @@ func (w *window) setVisible(env *C.JNIEnv) {
if width == 0 || height == 0 {
return
}
w.setStage(system.StageRunning)
w.visible = true
w.draw(env, true)
}
func (w *window) setStage(stage system.Stage) {
if stage == w.stage {
return
}
w.stage = stage
w.callbacks.Event(system.StageEvent{stage})
}
func (w *window) setVisual(visID int) error {
if C.ANativeWindow_setBuffersGeometry(w.win, 0, 0, C.int32_t(visID)) != 0 {
return errors.New("ANativeWindow_setBuffersGeometry failed")
@@ -826,10 +850,13 @@ func (w *window) SetAnimating(anim bool) {
}
func (w *window) draw(env *C.JNIEnv, sync bool) {
if !w.visible {
return
}
size := image.Pt(int(C.ANativeWindow_getWidth(w.win)), int(C.ANativeWindow_getHeight(w.win)))
if size != w.config.Size {
w.config.Size = size
w.callbacks.Event(ConfigEvent{Config: w.config})
w.processEvent(ConfigEvent{Config: w.config})
}
if size.X == 0 || size.Y == 0 {
return
@@ -837,14 +864,14 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
const inchPrDp = 1.0 / 160
ppdp := float32(w.dpi) * inchPrDp
dppp := unit.Dp(1.0 / ppdp)
insets := system.Insets{
insets := Insets{
Top: unit.Dp(w.insets.top) * dppp,
Bottom: unit.Dp(w.insets.bottom) * dppp,
Left: unit.Dp(w.insets.left) * dppp,
Right: unit.Dp(w.insets.right) * dppp,
}
w.callbacks.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Insets: insets,
@@ -898,8 +925,8 @@ func runInJVM(jvm *C.JavaVM, f func(env *C.JNIEnv)) {
f(env)
}
func convertKeyCode(code C.jint) (string, bool) {
var n string
func convertKeyCode(code C.jint) (key.Name, bool) {
var n key.Name
switch code {
case C.AKEYCODE_FORWARD_DEL:
n = key.NameDeleteForward
@@ -943,7 +970,7 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
if pressed == C.JNI_TRUE {
state = key.Press
}
w.callbacks.Event(key.Event{Name: n, State: state})
w.processEvent(key.Event{Name: n, State: state})
}
if pressed == C.JNI_TRUE && r != 0 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224).
w.callbacks.EditorInsert(string(rune(r)))
@@ -953,18 +980,18 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
//export Java_org_gioui_GioView_onTouchEvent
func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C.jlong, action, pointerID, tool C.jint, x, y, scrollX, scrollY C.jfloat, jbtns C.jint, t C.jlong) {
w := cgo.Handle(handle).Value().(*window)
var typ pointer.Type
var kind pointer.Kind
switch action {
case C.AMOTION_EVENT_ACTION_DOWN, C.AMOTION_EVENT_ACTION_POINTER_DOWN:
typ = pointer.Press
kind = pointer.Press
case C.AMOTION_EVENT_ACTION_UP, C.AMOTION_EVENT_ACTION_POINTER_UP:
typ = pointer.Release
kind = pointer.Release
case C.AMOTION_EVENT_ACTION_CANCEL:
typ = pointer.Cancel
kind = pointer.Cancel
case C.AMOTION_EVENT_ACTION_MOVE:
typ = pointer.Move
kind = pointer.Move
case C.AMOTION_EVENT_ACTION_SCROLL:
typ = pointer.Scroll
kind = pointer.Scroll
default:
return
}
@@ -993,8 +1020,8 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
default:
return
}
w.callbacks.Event(pointer.Event{
Type: typ,
w.processEvent(pointer.Event{
Kind: kind,
Source: src,
Buttons: btns,
PointerID: pointer.ID(pointerID),
@@ -1145,6 +1172,8 @@ func (w *window) ShowTextInput(show bool) {
}
func (w *window) SetInputHint(mode key.InputHint) {
w.inputHint = mode
// Constants defined at https://developer.android.com/reference/android/text/InputType.
const (
TYPE_NULL = 0
@@ -1288,17 +1317,14 @@ func findClass(env *C.JNIEnv, name string) C.jclass {
return C.jni_FindClass(env, cn)
}
func osMain() {
}
func newWindow(window *callbacks, options []Option) error {
func newWindow(window *callbacks, options []Option) {
mainWindow.in <- windowAndConfig{window, options}
return <-mainWindow.errs
<-mainWindow.windows
}
func (w *window) WriteClipboard(s string) {
func (w *window) WriteClipboard(mime string, s []byte) {
runInJVM(javaVM(), func(env *C.JNIEnv) {
jstr := javaString(env, s)
jstr := javaString(env, string(s))
callStaticVoidMethod(env, android.gioCls, android.mwriteClipboard,
jvalue(android.appCtx), jvalue(jstr))
})
@@ -1312,47 +1338,56 @@ func (w *window) ReadClipboard() {
return
}
content := goString(env, C.jstring(c))
w.callbacks.Event(clipboard.Event{Text: content})
w.processEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
})
}
func (w *window) Configure(options []Option) {
cnf := w.config
cnf.apply(unit.Metric{}, options)
runInJVM(javaVM(), func(env *C.JNIEnv) {
prev := w.config
cnf := w.config
cnf.apply(unit.Metric{}, options)
// Decorations are never disabled.
cnf.Decorated = true
if prev.Orientation != cnf.Orientation {
w.config.Orientation = cnf.Orientation
setOrientation(env, w.view, cnf.Orientation)
}
if prev.NavigationColor != cnf.NavigationColor {
w.config.NavigationColor = cnf.NavigationColor
setNavigationColor(env, w.view, cnf.NavigationColor)
}
if prev.StatusColor != cnf.StatusColor {
w.config.StatusColor = cnf.StatusColor
setStatusColor(env, w.view, cnf.StatusColor)
}
if prev.Mode != cnf.Mode {
switch cnf.Mode {
case Fullscreen:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE)
w.config.Mode = Fullscreen
case Windowed:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE)
w.config.Mode = Windowed
}
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.callbacks.Event(ConfigEvent{Config: w.config})
w.setConfig(env, cnf)
})
}
func (w *window) setConfig(env *C.JNIEnv, cnf Config) {
prev := w.config
// Decorations are never disabled.
cnf.Decorated = true
if prev.Orientation != cnf.Orientation {
w.config.Orientation = cnf.Orientation
setOrientation(env, w.view, cnf.Orientation)
}
if prev.NavigationColor != cnf.NavigationColor {
w.config.NavigationColor = cnf.NavigationColor
setNavigationColor(env, w.view, cnf.NavigationColor)
}
if prev.StatusColor != cnf.StatusColor {
w.config.StatusColor = cnf.StatusColor
setStatusColor(env, w.view, cnf.StatusColor)
}
if prev.Mode != cnf.Mode {
switch cnf.Mode {
case Fullscreen:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE)
w.config.Mode = Fullscreen
case Windowed:
callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE)
w.config.Mode = Windowed
}
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.processEvent(ConfigEvent{Config: w.config})
}
func (w *window) Perform(system.Action) {}
func (w *window) SetCursor(cursor pointer.Cursor) {
@@ -1361,9 +1396,10 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
})
}
func (w *window) Wakeup() {
func (w *window) wakeup() {
runOnMain(func(env *C.JNIEnv) {
w.callbacks.Event(wakeupEvent{})
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
@@ -1454,4 +1490,5 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
}
}
func (_ ViewEvent) ImplementsEvent() {}
func (AndroidViewEvent) implementsViewEvent() {}
func (AndroidViewEvent) ImplementsEvent() {}
+26 -21
View File
@@ -6,7 +6,7 @@ package app
#include <Foundation/Foundation.h>
__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"))) int gio_startDisplayLink(CFTypeRef dl);
__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
@@ -15,8 +15,8 @@ __attribute__ ((visibility ("hidden"))) void gio_hideCursor();
__attribute__ ((visibility ("hidden"))) void gio_showCursor();
__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
static bool isMainThread() {
return [NSThread isMainThread];
static int isMainThread() {
return [NSThread isMainThread] ? 1 : 0;
}
static NSUInteger nsstringLength(CFTypeRef cstr) {
@@ -42,7 +42,7 @@ static CFTypeRef newNSString(unichar *chars, NSUInteger length) {
import "C"
import (
"errors"
"runtime/cgo"
"sync"
"sync/atomic"
"time"
"unicode/utf16"
@@ -70,11 +70,14 @@ type displayLink struct {
running uint32
}
// displayLinks maps CFTypeRefs to *displayLinks.
var displayLinks sync.Map
var mainFuncs = make(chan func(), 1)
// runOnMain runs the function on the main thread.
func runOnMain(f func()) {
if C.isMainThread() {
if isMainThread() {
f()
return
}
@@ -84,6 +87,10 @@ func runOnMain(f func()) {
}()
}
func isMainThread() bool {
return C.isMainThread() != 0
}
//export gio_dispatchMainFuncs
func gio_dispatchMainFuncs() {
for {
@@ -121,25 +128,25 @@ func stringToNSString(str string) C.CFTypeRef {
return C.newNSString(chars, C.NSUInteger(len(u16)))
}
func NewDisplayLink(callback func()) (*displayLink, error) {
func newDisplayLink(callback func()) (*displayLink, error) {
d := &displayLink{
callback: callback,
done: make(chan struct{}),
states: make(chan bool),
dids: make(chan uint64),
}
h := cgo.NewHandle(d)
dl := C.gio_createDisplayLink(C.uintptr_t(h))
dl := C.gio_createDisplayLink()
if dl == 0 {
return nil, errors.New("app: failed to create display link")
}
go d.run(dl, h)
go d.run(dl)
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 h.Delete()
displayLinks.Store(dl, d)
defer displayLinks.Delete(dl)
var stopTimer *time.Timer
var tchan <-chan time.Time
started := false
@@ -200,10 +207,14 @@ func (d *displayLink) SetDisplayID(did uint64) {
}
//export gio_onFrameCallback
func gio_onFrameCallback(dl C.CFTypeRef, handle C.uintptr_t) {
d := cgo.Handle(handle).Value().(*displayLink)
if atomic.LoadUint32(&d.running) != 0 {
d.callback()
func gio_onFrameCallback(ref C.CFTypeRef) {
d, exists := displayLinks.Load(ref)
if !exists {
return
}
dl := d.(*displayLink)
if atomic.LoadUint32(&dl.running) != 0 {
dl.callback()
}
}
@@ -252,9 +263,3 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
C.gio_setCursor(C.NSUInteger(macosCursorID[to]))
return to
}
func (w *window) Wakeup() {
runOnMain(func() {
w.w.Event(wakeupEvent{})
})
}
-11
View File
@@ -1,11 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
#import <Foundation/Foundation.h>
#include "_cgo_export.h"
void gio_wakeupMainThread(void) {
dispatch_async(dispatch_get_main_queue(), ^{
gio_dispatchMainFuncs();
});
}
+118 -73
View File
@@ -12,6 +12,8 @@ package app
#include <UIKit/UIKit.h>
#include <stdint.h>
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
struct drawParams {
CGFloat dpi, sdpi;
CGFloat width, height;
@@ -72,21 +74,26 @@ import "C"
import (
"image"
"io"
"runtime"
"runtime/cgo"
"runtime/debug"
"strings"
"time"
"unicode/utf16"
"unsafe"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
)
type ViewEvent struct {
type UIKitViewEvent struct {
// ViewController is a CFTypeRef for the UIViewController backing a Window.
ViewController uintptr
}
@@ -95,18 +102,17 @@ type window struct {
view C.CFTypeRef
w *callbacks
displayLink *displayLink
loop *eventLoop
visible bool
cursor pointer.Cursor
config Config
hidden bool
cursor pointer.Cursor
config Config
pointerMap []C.CFTypeRef
}
var mainWindow = newWindowRendezvous()
var views = make(map[C.CFTypeRef]*window)
func init() {
// Darwin requires UI operations happen on the main thread only.
runtime.LockOSThread()
@@ -114,55 +120,59 @@ func init() {
//export onCreate
func onCreate(view, controller C.CFTypeRef) {
wopts := <-mainWindow.out
w := &window{
view: view,
w: wopts.window,
}
dl, err := NewDisplayLink(func() {
w.loop = newEventLoop(w.w, w.wakeup)
w.w.SetDriver(w)
mainWindow.windows <- struct{}{}
dl, err := newDisplayLink(func() {
w.draw(false)
})
if err != nil {
panic(err)
w.w.ProcessEvent(DestroyEvent{Err: err})
return
}
w.displayLink = dl
wopts := <-mainWindow.out
w.w = wopts.window
w.w.SetDriver(w)
views[view] = w
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
w.Configure(wopts.options)
w.w.Event(system.StageEvent{Stage: system.StagePaused})
w.w.Event(ViewEvent{ViewController: uintptr(controller)})
w.ProcessEvent(UIKitViewEvent{ViewController: uintptr(controller)})
}
func viewFor(h C.uintptr_t) *window {
return cgo.Handle(h).Value().(*window)
}
//export gio_onDraw
func gio_onDraw(view C.CFTypeRef) {
w := views[view]
func gio_onDraw(h C.uintptr_t) {
w := viewFor(h)
w.draw(true)
}
func (w *window) draw(sync bool) {
if w.hidden {
return
}
params := C.viewDrawParams(w.view)
if params.width == 0 || params.height == 0 {
return
}
wasVisible := w.visible
w.visible = true
if !wasVisible {
w.w.Event(system.StageEvent{Stage: system.StageRunning})
}
const inchPrDp = 1.0 / 163
m := unit.Metric{
PxPerDp: float32(params.dpi) * inchPrDp,
PxPerSp: float32(params.sdpi) * inchPrDp,
}
dppp := unit.Dp(1. / m.PxPerDp)
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: image.Point{
X: int(params.width + .5),
Y: int(params.height + .5),
},
Insets: system.Insets{
Insets: Insets{
Top: unit.Dp(params.top) * dppp,
Bottom: unit.Dp(params.bottom) * dppp,
Left: unit.Dp(params.left) * dppp,
@@ -175,26 +185,34 @@ func (w *window) draw(sync bool) {
}
//export onStop
func onStop(view C.CFTypeRef) {
w := views[view]
w.visible = false
w.w.Event(system.StageEvent{Stage: system.StagePaused})
func onStop(h C.uintptr_t) {
w := viewFor(h)
w.hidden = true
}
//export onStart
func onStart(h C.uintptr_t) {
w := viewFor(h)
w.hidden = false
w.draw(true)
}
//export onDestroy
func onDestroy(view C.CFTypeRef) {
w := views[view]
delete(views, view)
w.w.Event(ViewEvent{})
w.w.Event(system.DestroyEvent{})
func onDestroy(h C.uintptr_t) {
w := viewFor(h)
w.ProcessEvent(UIKitViewEvent{})
w.ProcessEvent(DestroyEvent{})
w.displayLink.Close()
w.displayLink = nil
cgo.Handle(h).Delete()
w.view = 0
}
//export onFocus
func onFocus(view C.CFTypeRef, focus int) {
w := views[view]
w.w.Event(key.FocusEvent{Focus: focus != 0})
func onFocus(h C.uintptr_t, focus int) {
w := viewFor(h)
w.config.Focused = focus != 0
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export onLowMemory
@@ -204,56 +222,56 @@ func onLowMemory() {
}
//export onUpArrow
func onUpArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameUpArrow)
func onUpArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameUpArrow)
}
//export onDownArrow
func onDownArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameDownArrow)
func onDownArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameDownArrow)
}
//export onLeftArrow
func onLeftArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameLeftArrow)
func onLeftArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameLeftArrow)
}
//export onRightArrow
func onRightArrow(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameRightArrow)
func onRightArrow(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameRightArrow)
}
//export onDeleteBackward
func onDeleteBackward(view C.CFTypeRef) {
views[view].onKeyCommand(key.NameDeleteBackward)
func onDeleteBackward(h C.uintptr_t) {
viewFor(h).onKeyCommand(key.NameDeleteBackward)
}
//export onText
func onText(view, str C.CFTypeRef) {
w := views[view]
func onText(h C.uintptr_t, str C.CFTypeRef) {
w := viewFor(h)
w.w.EditorInsert(nsstringToString(str))
}
//export onTouch
func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
var typ pointer.Type
func onTouch(h C.uintptr_t, last C.int, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.CGFloat, ti C.double) {
var kind pointer.Kind
switch phase {
case C.UITouchPhaseBegan:
typ = pointer.Press
kind = pointer.Press
case C.UITouchPhaseMoved:
typ = pointer.Move
kind = pointer.Move
case C.UITouchPhaseEnded:
typ = pointer.Release
kind = pointer.Release
case C.UITouchPhaseCancelled:
typ = pointer.Cancel
kind = pointer.Cancel
default:
return
}
w := views[view]
w := viewFor(h)
t := time.Duration(float64(ti) * float64(time.Second))
p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{
Type: typ,
w.ProcessEvent(pointer.Event{
Kind: kind,
Source: pointer.Touch,
PointerID: w.lookupTouch(last != 0, touchRef),
Position: p,
@@ -265,11 +283,16 @@ func (w *window) ReadClipboard() {
cstr := C.readClipboard()
defer C.CFRelease(cstr)
content := nsstringToString(cstr)
w.w.Event(clipboard.Event{Text: content})
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
}
func (w *window) WriteClipboard(s string) {
u16 := utf16.Encode([]rune(s))
func (w *window) WriteClipboard(mime string, s []byte) {
u16 := utf16.Encode([]rune(string(s)))
var chars *C.unichar
if len(u16) > 0 {
chars = (*C.unichar)(unsafe.Pointer(&u16[0]))
@@ -280,7 +303,7 @@ func (w *window) WriteClipboard(s string) {
func (w *window) Configure([]Option) {
// Decorations are never disabled.
w.config.Decorated = true
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *window) EditorStateChanged(old, new editorState) {}
@@ -288,10 +311,6 @@ func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) Perform(system.Action) {}
func (w *window) SetAnimating(anim bool) {
v := w.view
if v == 0 {
return
}
if anim {
w.displayLink.Start()
} else {
@@ -303,8 +322,8 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
w.cursor = windowSetCursor(w.cursor, cursor)
}
func (w *window) onKeyCommand(name string) {
w.w.Event(key.Event{
func (w *window) onKeyCommand(name key.Name) {
w.ProcessEvent(key.Event{
Name: name,
})
}
@@ -343,12 +362,30 @@ func (w *window) ShowTextInput(show bool) {
func (w *window) SetInputHint(_ key.InputHint) {}
func newWindow(win *callbacks, options []Option) error {
mainWindow.in <- windowAndConfig{win, options}
return <-mainWindow.errs
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.loop.FlushEvents()
}
func osMain() {
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
}
//export gio_runMain
@@ -356,4 +393,12 @@ func gio_runMain() {
runMain()
}
func (_ ViewEvent) ImplementsEvent() {}
func (w *window) wakeup() {
runOnMain(func() {
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
func (UIKitViewEvent) implementsViewEvent() {}
func (UIKitViewEvent) ImplementsEvent() {}
+37 -38
View File
@@ -11,6 +11,7 @@
__attribute__ ((visibility ("hidden"))) Class gio_layerClass(void);
@interface GioView: UIView <UIKeyInput>
@property uintptr_t handle;
@end
@implementation GioViewController
@@ -54,33 +55,33 @@ CGFloat _keyboardHeight;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
UIView *drawView = self.view.subviews[0];
if (drawView != nil) {
gio_onDraw((__bridge CFTypeRef)drawView);
GioView *view = (GioView *)self.view.subviews[0];
if (view != nil) {
onStart(view.handle);
}
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
UIView *drawView = self.view.subviews[0];
if (drawView != nil) {
onStop((__bridge CFTypeRef)drawView);
GioView *view = (GioView *)self.view.subviews[0];
if (view != nil) {
onStop(view.handle);
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
CFTypeRef viewRef = (__bridge CFTypeRef)self.view.subviews[0];
onDestroy(viewRef);
GioView *view = (GioView *)self.view.subviews[0];
onDestroy(view.handle);
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
UIView *view = self.view.subviews[0];
GioView *view = (GioView *)self.view.subviews[0];
CGRect frame = self.view.bounds;
// Adjust view bounds to make room for the keyboard.
frame.size.height -= _keyboardHeight;
view.frame = frame;
gio_onDraw((__bridge CFTypeRef)view);
gio_onDraw(view.handle);
}
- (void)didReceiveMemoryWarning {
@@ -101,11 +102,10 @@ CGFloat _keyboardHeight;
}
@end
static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIEvent *event) {
static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UIEvent *event) {
CGFloat scale = view.contentScaleFactor;
NSUInteger i = 0;
NSUInteger n = [touches count];
CFTypeRef viewRef = (__bridge CFTypeRef)view;
for (UITouch *touch in touches) {
CFTypeRef touchRef = (__bridge CFTypeRef)touch;
i++;
@@ -116,13 +116,16 @@ static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIE
CGPoint loc = [coalescedTouch locationInView:view];
j++;
int lastTouch = last && i == n && j == m;
onTouch(lastTouch, viewRef, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
onTouch(view.handle, lastTouch, touchRef, touch.phase, loc.x*scale, loc.y*scale, [coalescedTouch timestamp]);
}
}
}
@implementation GioView
NSArray<UIKeyCommand *> *_keyCommands;
+ (void)onFrameCallback:(CADisplayLink *)link {
gio_onFrameCallback((__bridge CFTypeRef)link);
}
+ (Class)layerClass {
return gio_layerClass();
}
@@ -148,13 +151,13 @@ NSArray<UIKeyCommand *> *_keyCommands;
- (void)onWindowDidBecomeKey:(NSNotification *)note {
if (self.isFirstResponder) {
onFocus((__bridge CFTypeRef)self, YES);
onFocus(self.handle, YES);
}
}
- (void)onWindowDidResignKey:(NSNotification *)note {
if (self.isFirstResponder) {
onFocus((__bridge CFTypeRef)self, NO);
onFocus(self.handle, NO);
}
}
@@ -175,7 +178,7 @@ NSArray<UIKeyCommand *> *_keyCommands;
}
- (void)insertText:(NSString *)text {
onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)text);
onText(self.handle, (__bridge CFTypeRef)text);
}
- (BOOL)canBecomeFirstResponder {
@@ -187,23 +190,23 @@ NSArray<UIKeyCommand *> *_keyCommands;
}
- (void)deleteBackward {
onDeleteBackward((__bridge CFTypeRef)self);
onDeleteBackward(self.handle);
}
- (void)onUpArrow {
onUpArrow((__bridge CFTypeRef)self);
onUpArrow(self.handle);
}
- (void)onDownArrow {
onDownArrow((__bridge CFTypeRef)self);
onDownArrow(self.handle);
}
- (void)onLeftArrow {
onLeftArrow((__bridge CFTypeRef)self);
onLeftArrow(self.handle);
}
- (void)onRightArrow {
onRightArrow((__bridge CFTypeRef)self);
onRightArrow(self.handle);
}
- (NSArray<UIKeyCommand *> *)keyCommands {
@@ -227,23 +230,8 @@ NSArray<UIKeyCommand *> *_keyCommands;
}
@end
@interface DisplayLinkHandle : NSObject {
}
@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:)];
CFTypeRef gio_createDisplayLink(void) {
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)];
dl.paused = YES;
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
[dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
@@ -283,3 +271,14 @@ void gio_showCursor() {
void gio_setCursor(NSUInteger curID) {
// Not supported.
}
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
GioView *v = (__bridge GioView *)viewRef;
v.handle = handle;
}
void gio_wakeupMainThread(void) {
dispatch_async(dispatch_get_main_queue(), ^{
gio_dispatchMainFuncs();
});
}
+101 -105
View File
@@ -6,6 +6,7 @@ import (
"fmt"
"image"
"image/color"
"io"
"strings"
"syscall/js"
"time"
@@ -13,16 +14,18 @@ import (
"unicode/utf8"
"gioui.org/internal/f32color"
"gioui.org/op"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/unit"
)
type ViewEvent struct {
type JSViewEvent struct {
Element js.Value
}
@@ -53,9 +56,6 @@ type window struct {
composing bool
requestFocus bool
chanAnimation chan struct{}
chanRedraw chan struct{}
config Config
inset f32.Point
scale float32
@@ -68,7 +68,7 @@ type window struct {
contextStatus contextStatus
}
func newWindow(win *callbacks, options []Option) error {
func newWindow(win *callbacks, options []Option) {
doc := js.Global().Get("document")
cont := getContainer(doc)
cnv := createCanvas(doc)
@@ -83,7 +83,9 @@ func newWindow(win *callbacks, options []Option) error {
head: doc.Get("head"),
clipboard: js.Global().Get("navigator").Get("clipboard"),
wakeups: make(chan struct{}, 1),
w: win,
}
w.w.SetDriver(w)
w.requestAnimationFrame = w.window.Get("requestAnimationFrame")
w.browserHistory = w.window.Get("history")
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() {
w.screenOrientation = screen.Get("orientation")
}
w.chanAnimation = make(chan struct{}, 1)
w.chanRedraw = make(chan struct{}, 1)
w.redraw = w.funcOf(func(this js.Value, args []js.Value) interface{} {
w.chanAnimation <- struct{}{}
w.draw(false)
return nil
})
w.clipboardCallback = w.funcOf(func(this js.Value, args []js.Value) interface{} {
content := args[0].String()
go win.Event(clipboard.Event{Text: content})
w.processEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
return nil
})
w.addEventListeners()
w.addHistory()
w.w = win
go func() {
defer w.cleanup()
w.w.SetDriver(w)
w.Configure(options)
w.blur()
w.w.Event(ViewEvent{Element: cont})
w.w.Event(system.StageEvent{Stage: system.StageRunning})
w.resize()
w.draw(true)
for {
select {
case <-w.wakeups:
w.w.Event(wakeupEvent{})
case <-w.chanAnimation:
w.animCallback()
case <-w.chanRedraw:
w.draw(true)
}
}
}()
return nil
w.Configure(options)
w.blur()
w.processEvent(JSViewEvent{Element: cont})
w.resize()
w.draw(true)
}
func getContainer(doc js.Value) js.Value {
@@ -188,12 +176,12 @@ func (w *window) addEventListeners() {
w.cnv.Set("width", 0)
w.cnv.Set("height", 0)
w.resize()
w.requestRedraw()
w.draw(true)
return nil
})
w.addEventListener(w.visualViewport, "resize", func(this js.Value, args []js.Value) interface{} {
w.resize()
w.requestRedraw()
w.draw(true)
return nil
})
w.addEventListener(w.window, "contextmenu", func(this js.Value, args []js.Value) interface{} {
@@ -201,22 +189,11 @@ func (w *window) addEventListeners() {
return nil
})
w.addEventListener(w.window, "popstate", func(this js.Value, args []js.Value) interface{} {
if w.w.Event(key.Event{Name: key.NameBack}) {
if w.processEvent(key.Event{Name: key.NameBack}) {
return w.browserHistory.Call("forward")
}
return w.browserHistory.Call("back")
})
w.addEventListener(w.document, "visibilitychange", func(this js.Value, args []js.Value) interface{} {
ev := system.StageEvent{}
switch w.document.Get("visibilityState").String() {
case "hidden", "prerender", "unloaded":
ev.Stage = system.StagePaused
default:
ev.Stage = system.StageRunning
}
w.w.Event(ev)
return nil
})
w.addEventListener(w.cnv, "mousemove", func(this js.Value, args []js.Value) interface{} {
w.pointerEvent(pointer.Move, 0, 0, args[0])
return nil
@@ -274,18 +251,20 @@ func (w *window) addEventListeners() {
w.touches[i] = js.Null()
}
w.touches = w.touches[:0]
w.w.Event(pointer.Event{
Type: pointer.Cancel,
w.processEvent(pointer.Event{
Kind: pointer.Cancel,
Source: pointer.Touch,
})
return nil
})
w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
w.w.Event(key.FocusEvent{Focus: true})
w.config.Focused = true
w.processEvent(ConfigEvent{Config: w.config})
return nil
})
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
w.w.Event(key.FocusEvent{Focus: false})
w.config.Focused = false
w.processEvent(ConfigEvent{Config: w.config})
w.blur()
return nil
})
@@ -374,10 +353,50 @@ func (w *window) keyEvent(e js.Value, ks key.State) {
Modifiers: modifiersFor(e),
State: ks,
}
w.w.Event(cmd)
w.processEvent(cmd)
}
}
func (w *window) ProcessEvent(e event.Event) {
w.processEvent(e)
}
func (w *window) processEvent(e event.Event) bool {
if !w.w.ProcessEvent(e) {
return false
}
select {
case w.wakeups <- struct{}{}:
default:
}
return true
}
func (w *window) Event() event.Event {
for {
evt, ok := w.w.nextEvent()
if ok {
if _, destroy := evt.(DestroyEvent); destroy {
w.cleanup()
}
return evt
}
<-w.wakeups
}
}
func (w *window) Invalidate() {
w.w.Invalidate()
}
func (w *window) Run(f func()) {
f()
}
func (w *window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
}
// modifiersFor returns the modifier set for a DOM MouseEvent or
// KeyEvent.
func modifiersFor(e js.Value) key.Modifiers {
@@ -398,7 +417,7 @@ func modifiersFor(e js.Value) key.Modifiers {
return mods
}
func (w *window) touchEvent(typ pointer.Type, e js.Value) {
func (w *window) touchEvent(kind pointer.Kind, e js.Value) {
e.Call("preventDefault")
t := time.Duration(e.Get("timeStamp").Int()) * time.Millisecond
changedTouches := e.Get("changedTouches")
@@ -425,8 +444,8 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) {
X: float32(x) * scale,
Y: float32(y) * scale,
}
w.w.Event(pointer.Event{
Type: typ,
w.processEvent(pointer.Event{
Kind: kind,
Source: pointer.Touch,
Position: pos,
PointerID: pid,
@@ -448,7 +467,7 @@ func (w *window) touchIDFor(touch js.Value) pointer.ID {
return pid
}
func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
func (w *window) pointerEvent(kind pointer.Kind, dx, dy float32, e js.Value) {
e.Call("preventDefault")
x, y := e.Get("clientX").Float(), e.Get("clientY").Float()
rect := w.cnv.Call("getBoundingClientRect")
@@ -475,8 +494,8 @@ func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
if jbtns&4 != 0 {
btns |= pointer.ButtonTertiary
}
w.w.Event(pointer.Event{
Type: typ,
w.processEvent(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Buttons: btns,
Position: pos,
@@ -502,17 +521,6 @@ func (w *window) funcOf(f func(this js.Value, args []js.Value) interface{}) js.F
return jsf
}
func (w *window) animCallback() {
anim := w.animating
w.animRequested = anim
if anim {
w.requestAnimationFrame.Invoke(w.redraw)
}
if anim {
w.draw(false)
}
}
func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) SetAnimating(anim bool) {
@@ -533,14 +541,14 @@ func (w *window) ReadClipboard() {
w.clipboard.Call("readText", w.clipboard).Call("then", w.clipboardCallback)
}
func (w *window) WriteClipboard(s string) {
func (w *window) WriteClipboard(mime string, s []byte) {
if w.clipboard.IsUndefined() {
return
}
if w.clipboard.Get("writeText").IsUndefined() {
return
}
w.clipboard.Call("writeText", s)
w.clipboard.Call("writeText", string(s))
}
func (w *window) Configure(options []Option) {
@@ -568,7 +576,7 @@ func (w *window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.w.Event(ConfigEvent{Config: w.config})
w.processEvent(ConfigEvent{Config: w.config})
}
func (w *window) Perform(system.Action) {}
@@ -607,23 +615,14 @@ func (w *window) SetCursor(cursor pointer.Cursor) {
style.Set("cursor", webCursor[cursor])
}
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
}
func (w *window) ShowTextInput(show bool) {
// Run in a goroutine to avoid a deadlock if the
// focus change result in an event.
go func() {
if show {
w.focus()
} else {
w.blur()
}
}()
if show {
w.focus()
} else {
w.blur()
}
}
func (w *window) SetInputHint(mode key.InputHint) {
@@ -640,7 +639,7 @@ func (w *window) resize() {
}
if size != w.config.Size {
w.config.Size = size
w.w.Event(ConfigEvent{Config: w.config})
w.processEvent(ConfigEvent{Config: w.config})
}
if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() {
@@ -660,13 +659,20 @@ func (w *window) draw(sync bool) {
if w.contextStatus == contextStatusLost {
return
}
anim := w.animating
w.animRequested = anim
if anim {
w.requestAnimationFrame.Invoke(w.redraw)
} else if !sync {
return
}
size, insets, metric := w.getConfig()
if metric == (unit.Metric{}) || size.X == 0 || size.Y == 0 {
return
}
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.processEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: size,
Insets: insets,
@@ -676,10 +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)
return image.Pt(w.config.Size.X, w.config.Size.Y),
system.Insets{
Insets{
Bottom: unit.Dp(w.inset.Y) * invscale,
Right: unit.Dp(w.inset.X) * invscale,
}, unit.Metric{
@@ -735,19 +741,8 @@ func (w *window) navigationColor(c color.NRGBA) {
theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
}
func (w *window) requestRedraw() {
select {
case w.chanRedraw <- struct{}{}:
default:
}
}
func osMain() {
select {}
}
func translateKey(k string) (string, bool) {
var n string
func translateKey(k string) (key.Name, bool) {
var n key.Name
switch k {
case "ArrowUp":
@@ -814,11 +809,12 @@ func translateKey(k string) (string, bool) {
r, s := utf8.DecodeRuneInString(k)
// If there is exactly one printable character, return that.
if s == len(k) && unicode.IsPrint(r) {
return strings.ToUpper(k), true
return key.Name(strings.ToUpper(k)), true
}
return "", false
}
return n, true
}
func (_ ViewEvent) ImplementsEvent() {}
func (JSViewEvent) implementsViewEvent() {}
func (JSViewEvent) ImplementsEvent() {}
+342 -225
View File
@@ -8,16 +8,21 @@ package app
import (
"errors"
"image"
"io"
"runtime"
"runtime/cgo"
"strings"
"time"
"unicode"
"unicode/utf8"
"gioui.org/internal/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
_ "gioui.org/internal/cocoainit"
@@ -34,9 +39,10 @@ import (
#define MOUSE_DOWN 3
#define MOUSE_SCROLL 4
__attribute__ ((visibility ("hidden"))) void gio_main(void);
__attribute__ ((visibility ("hidden"))) void gio_initApp(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void);
__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) {
@autoreleasepool {
@@ -56,144 +62,204 @@ static CFTypeRef readClipboard(void) {
}
static CGFloat viewHeight(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.height;
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.height;
}
}
static CGFloat viewWidth(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.width;
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return [view bounds].size.width;
}
}
static CGFloat getScreenBackingScale(void) {
return [NSScreen.mainScreen backingScaleFactor];
@autoreleasepool {
return [NSScreen.mainScreen backingScaleFactor];
}
}
static CGFloat getViewBackingScale(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef;
return [view.window backingScaleFactor];
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return [view.window backingScaleFactor];
}
}
static void setNeedsDisplay(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef;
[view setNeedsDisplay:YES];
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
[view setNeedsDisplay:YES];
}
}
static NSPoint cascadeTopLeftFromPoint(CFTypeRef windowRef, NSPoint topLeft) {
NSWindow *window = (__bridge NSWindow *)windowRef;
return [window cascadeTopLeftFromPoint:topLeft];
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return [window cascadeTopLeftFromPoint:topLeft];
}
}
static void makeKeyAndOrderFront(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
}
}
static void toggleFullScreen(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window toggleFullScreen:nil];
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window toggleFullScreen:nil];
}
}
static NSWindowStyleMask getWindowStyleMask(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef;
return [window styleMask];
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return [window styleMask];
}
}
static void setWindowStyleMask(CFTypeRef windowRef, NSWindowStyleMask mask) {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.styleMask = mask;
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.styleMask = mask;
}
}
static void setWindowTitleVisibility(CFTypeRef windowRef, NSWindowTitleVisibility state) {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.titleVisibility = state;
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.titleVisibility = state;
}
}
static void setWindowTitlebarAppearsTransparent(CFTypeRef windowRef, int transparent) {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.titlebarAppearsTransparent = (BOOL)transparent;
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.titlebarAppearsTransparent = (BOOL)transparent;
}
}
static void setWindowStandardButtonHidden(CFTypeRef windowRef, NSWindowButton btn, int hide) {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window standardWindowButton:btn].hidden = (BOOL)hide;
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window standardWindowButton:btn].hidden = (BOOL)hide;
}
}
static void performWindowDragWithEvent(CFTypeRef windowRef, CFTypeRef evt) {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window performWindowDragWithEvent:(__bridge NSEvent*)evt];
}
}
static void closeWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window performClose:nil];
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window performClose:nil];
}
}
static void setSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
NSWindow* window = (__bridge NSWindow *)windowRef;
NSSize size = NSMakeSize(width, height);
[window setContentSize:size];
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
NSSize size = NSMakeSize(width, height);
[window setContentSize:size];
}
}
static void setMinSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMinSize = NSMakeSize(width, height);
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMinSize = NSMakeSize(width, height);
}
}
static void setMaxSize(CFTypeRef windowRef, CGFloat width, CGFloat height) {
NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMaxSize = NSMakeSize(width, height);
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
window.contentMaxSize = NSMakeSize(width, height);
}
}
static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
NSWindow* window = (__bridge NSWindow *)windowRef;
NSRect r = NSMakeRect(x, y, w, h);
[window setFrame:r display:YES];
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
NSRect r = NSMakeRect(x, y, w, h);
[window setFrame:r display:YES];
}
}
static void hideWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window miniaturize:window];
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window miniaturize:window];
}
}
static void unhideWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window deminiaturize:window];
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window deminiaturize:window];
}
}
static NSRect getScreenFrame(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef;
return [[window screen] frame];
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
return [[window screen] frame];
}
}
static void setTitle(CFTypeRef windowRef, CFTypeRef titleRef) {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.title = (__bridge NSString *)titleRef;
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
window.title = (__bridge NSString *)titleRef;
}
}
static int isWindowZoomed(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef;
return window.zoomed ? 1 : 0;
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return window.zoomed ? 1 : 0;
}
}
static void zoomWindow(CFTypeRef windowRef) {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window zoom:nil];
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
[window zoom:nil];
}
}
static CFTypeRef layerForView(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.layer;
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.layer;
}
}
static CFTypeRef windowForView(CFTypeRef viewRef) {
NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.window;
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
return (__bridge CFTypeRef)view.window;
}
}
static void raiseWindow(CFTypeRef windowRef) {
NSWindow* window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
@autoreleasepool {
NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
if (![currentApp isActive]) {
[currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
}
}
static CFTypeRef createInputContext(CFTypeRef clientRef) {
@@ -205,23 +271,33 @@ static CFTypeRef createInputContext(CFTypeRef clientRef) {
}
static void discardMarkedText(CFTypeRef viewRef) {
@autoreleasepool {
@autoreleasepool {
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
if (view == [ctx client]) {
[ctx discardMarkedText];
}
}
}
}
static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
@autoreleasepool {
@autoreleasepool {
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
if (view == [ctx client]) {
[ctx invalidateCharacterCoordinates];
}
}
}
}
static void dispatchEvent(void) {
@autoreleasepool {
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantFuture]
inMode:NSDefaultRunLoopMode
dequeue:YES];
[NSApp sendEvent:event];
}
}
*/
import "C"
@@ -231,9 +307,9 @@ func init() {
runtime.LockOSThread()
}
// ViewEvent notified the client of changes to the window AppKit handles.
// The handles are retained until another ViewEvent is sent.
type ViewEvent struct {
// AppKitViewEvent notifies the client of changes to the window AppKit handles.
// The handles are retained until another AppKitViewEvent is sent.
type AppKitViewEvent struct {
// View is a CFTypeRef for the NSView for the window.
View uintptr
// Layer is a CFTypeRef of the CALayer of View.
@@ -243,52 +319,32 @@ type ViewEvent struct {
type window struct {
view C.CFTypeRef
w *callbacks
stage system.Stage
anim bool
visible bool
displayLink *displayLink
// redraw is a single entry channel for making sure only one
// display link redraw request is in flight.
redraw chan struct{}
cursor pointer.Cursor
pointerBtns pointer.Buttons
loop *eventLoop
scale float32
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 after gio_initApp returns.
var launched = make(chan struct{})
// nextTopLeft is the offset to use for the next window's call to
// cascadeTopLeftFromPoint.
var nextTopLeft C.NSPoint
// mustView is like lookupView, except that it panics
// if the view isn't mapped.
func mustView(view C.CFTypeRef) *window {
w, ok := lookupView(view)
if !ok {
panic("no window for view")
}
return w
}
// mainThreadWindow is the window currently in control of the main thread.
var mainThreadWindow *window
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 windowFor(h C.uintptr_t) *window {
return cgo.Handle(h).Value().(*window)
}
func (w *window) contextView() C.CFTypeRef {
@@ -297,13 +353,20 @@ func (w *window) contextView() C.CFTypeRef {
func (w *window) ReadClipboard() {
cstr := C.readClipboard()
defer C.CFRelease(cstr)
if cstr != 0 {
defer C.CFRelease(cstr)
}
content := nsstringToString(cstr)
w.w.Event(clipboard.Event{Text: content})
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
}
func (w *window) WriteClipboard(s string) {
cstr := stringToNSString(s)
func (w *window) WriteClipboard(mime string, s []byte) {
cstr := stringToNSString(string(s))
defer C.CFRelease(cstr)
C.writeClipboard(cstr)
}
@@ -365,11 +428,11 @@ func (w *window) Configure(options []Option) {
case Minimized:
C.unhideWindow(window)
case Maximized:
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
}
w.config.Mode = Windowed
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
w.setTitle(prev, cnf)
if prev.Size != cnf.Size {
w.config.Size = cnf.Size
@@ -407,7 +470,7 @@ func (w *window) Configure(options []Option) {
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
}
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *window) setTitle(prev, cnf Config) {
@@ -458,7 +521,8 @@ func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) SetAnimating(anim bool) {
if anim {
w.anim = anim
if w.anim && w.visible {
w.displayLink.Start()
} else {
w.displayLink.Stop()
@@ -475,26 +539,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
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)
kmods := convertMods(mods)
ks := key.Release
if keyDown {
ks = key.Press
}
w := mustView(view)
w := windowFor(h)
for _, k := range str {
if n, ok := convertKey(k); ok {
w.w.Event(key.Event{
w.ProcessEvent(key.Event{
Name: n,
Modifiers: kmods,
State: ks,
@@ -504,15 +560,15 @@ func gio_onKeys(view, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown
}
//export gio_onText
func gio_onText(view, cstr C.CFTypeRef) {
func gio_onText(h C.uintptr_t, cstr C.CFTypeRef) {
str := nsstringToString(cstr)
w := mustView(view)
w := windowFor(h)
w.w.EditorInsert(str)
}
//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) {
w := mustView(view)
func gio_onMouse(h C.uintptr_t, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
w := windowFor(h)
t := time.Duration(float64(ti)*float64(time.Second) + .5)
xf, yf := float32(x)*w.scale, float32(y)*w.scale
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
@@ -523,8 +579,10 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
btn = pointer.ButtonPrimary
case 1:
btn = pointer.ButtonSecondary
case 2:
btn = pointer.ButtonTertiary
}
var typ pointer.Type
var typ pointer.Kind
switch cdir {
case C.MOUSE_MOVE:
typ = pointer.Move
@@ -547,8 +605,8 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
default:
panic("invalid direction")
}
w.w.Event(pointer.Event{
Type: typ,
w.ProcessEvent(pointer.Event{
Kind: typ,
Source: pointer.Mouse,
Time: t,
Buttons: w.pointerBtns,
@@ -559,35 +617,29 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
}
//export gio_onDraw
func gio_onDraw(view C.CFTypeRef) {
w := mustView(view)
func gio_onDraw(h C.uintptr_t) {
w := windowFor(h)
w.draw()
}
//export gio_onFocus
func gio_onFocus(view C.CFTypeRef, focus C.int) {
w := mustView(view)
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)
}
}
func gio_onFocus(h C.uintptr_t, focus C.int) {
w := windowFor(h)
w.SetCursor(w.cursor)
w.config.Focused = focus == 1
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onChangeScreen
func gio_onChangeScreen(view C.CFTypeRef, did uint64) {
w := mustView(view)
func gio_onChangeScreen(h C.uintptr_t, did uint64) {
w := windowFor(h)
w.displayLink.SetDisplayID(did)
C.setNeedsDisplay(w.view)
}
//export gio_hasMarkedText
func gio_hasMarkedText(view C.CFTypeRef) C.int {
w := mustView(view)
func gio_hasMarkedText(h C.uintptr_t) C.int {
w := windowFor(h)
state := w.w.EditorState()
if state.compose.Start != -1 {
return 1
@@ -596,8 +648,8 @@ func gio_hasMarkedText(view C.CFTypeRef) C.int {
}
//export gio_markedRange
func gio_markedRange(view C.CFTypeRef) C.NSRange {
w := mustView(view)
func gio_markedRange(h C.uintptr_t) C.NSRange {
w := windowFor(h)
state := w.w.EditorState()
rng := state.compose
start, end := rng.Start, rng.End
@@ -612,8 +664,8 @@ func gio_markedRange(view C.CFTypeRef) C.NSRange {
}
//export gio_selectedRange
func gio_selectedRange(view C.CFTypeRef) C.NSRange {
w := mustView(view)
func gio_selectedRange(h C.uintptr_t) C.NSRange {
w := windowFor(h)
state := w.w.EditorState()
rng := state.Selection
start, end := rng.Start, rng.End
@@ -628,14 +680,14 @@ func gio_selectedRange(view C.CFTypeRef) C.NSRange {
}
//export gio_unmarkText
func gio_unmarkText(view C.CFTypeRef) {
w := mustView(view)
func gio_unmarkText(h C.uintptr_t) {
w := windowFor(h)
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
}
//export gio_setMarkedText
func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
w := mustView(view)
func gio_setMarkedText(h C.uintptr_t, cstr C.CFTypeRef, selRange C.NSRange, replaceRange C.NSRange) {
w := windowFor(h)
str := nsstringToString(cstr)
state := w.w.EditorState()
rng := state.compose
@@ -674,8 +726,8 @@ func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange
}
//export gio_substringForProposedRange
func gio_substringForProposedRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
w := mustView(view)
func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.CFTypeRef {
w := windowFor(h)
state := w.w.EditorState()
start, end := state.Snippet.Start, state.Snippet.End
if start > end {
@@ -695,8 +747,8 @@ func gio_substringForProposedRange(view C.CFTypeRef, crng C.NSRange, actual C.NS
}
//export gio_insertText
func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) {
w := mustView(view)
func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
w := windowFor(h)
state := w.w.EditorState()
rng := state.compose
if rng.Start == -1 {
@@ -720,13 +772,13 @@ func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) {
}
//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
}
//export gio_firstRectForCharacterRange
func gio_firstRectForCharacterRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
w := mustView(view)
func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
w := windowFor(h)
state := w.w.EditorState()
sel := state.Selection
u16start := state.UTF16Index(sel.Start)
@@ -753,6 +805,10 @@ func (w *window) draw() {
case <-w.redraw:
default:
}
w.visible = true
if w.anim {
w.SetAnimating(w.anim)
}
w.scale = float32(C.getViewBackingScale(w.view))
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
sz := image.Point{
@@ -761,15 +817,14 @@ func (w *window) draw() {
}
if sz != w.config.Size {
w.config.Size = sz
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
if sz.X == 0 || sz.Y == 0 {
return
}
cfg := configFor(w.scale)
w.setStage(system.StageRunning)
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: cfg,
@@ -778,6 +833,55 @@ func (w *window) draw() {
})
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
// The main thread window deliver events in Event.
if w != mainThreadWindow {
w.loop.FlushEvents()
}
}
func (w *window) Event() event.Event {
if !isMainThread() {
return w.loop.Event()
}
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
for {
if evt, ok := w.loop.win.nextEvent(); ok {
return evt
}
C.dispatchEvent()
gio_dispatchMainFuncs()
}
}
func (w *window) Invalidate() {
if isMainThread() {
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
}
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
if isMainThread() {
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
}
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
if !isMainThread() {
w.loop.Frame(frame)
return
}
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
w.loop.win.ProcessFrame(frame, nil)
}
func configFor(scale float32) unit.Metric {
return unit.Metric{
PxPerDp: scale,
@@ -785,77 +889,87 @@ func configFor(scale float32) unit.Metric {
}
}
//export gio_onClose
func gio_onClose(view C.CFTypeRef) {
w := mustView(view)
//export gio_onAttached
func gio_onAttached(h C.uintptr_t, attached C.int) {
w := windowFor(h)
if attached != 0 {
layer := C.layerForView(w.view)
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
} else {
w.ProcessEvent(AppKitViewEvent{})
w.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.w.Event(ViewEvent{})
deleteView(view)
w.w.Event(system.DestroyEvent{})
C.CFRelease(w.view)
w.view = 0
w.displayLink = nil
cgo.Handle(h).Delete()
w.view = 0
}
//export gio_onHide
func gio_onHide(view C.CFTypeRef) {
w := mustView(view)
w.setStage(system.StagePaused)
func gio_onHide(h C.uintptr_t) {
w := windowFor(h)
w.visible = false
w.SetAnimating(w.anim)
}
//export gio_onShow
func gio_onShow(view C.CFTypeRef) {
w := mustView(view)
w.setStage(system.StageRunning)
func gio_onShow(h C.uintptr_t) {
w := windowFor(h)
w.draw()
}
//export gio_onFullscreen
func gio_onFullscreen(view C.CFTypeRef) {
w := mustView(view)
func gio_onFullscreen(h C.uintptr_t) {
w := windowFor(h)
w.config.Mode = Fullscreen
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onWindowed
func gio_onWindowed(view C.CFTypeRef) {
w := mustView(view)
func gio_onWindowed(h C.uintptr_t) {
w := windowFor(h)
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)
func newWindow(win *callbacks, options []Option) {
w := &window{
redraw: make(chan struct{}, 1),
w: win,
}
}
//export gio_onAppShow
func gio_onAppShow() {
for _, w := range viewMap {
w.setStage(system.StageRunning)
w.loop = newEventLoop(w.w, w.wakeup)
if isMainThread() {
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
select {
case <-launched:
default:
// If we're the main thread, initialize the GUI.
C.gio_initApp()
close(launched)
}
} else {
<-launched
}
}
//export gio_onFinishLaunching
func gio_onFinishLaunching() {
close(launched)
}
func newWindow(win *callbacks, options []Option) error {
<-launched
errch := make(chan error)
res := make(chan struct{}, 1)
runOnMain(func() {
w, err := newOSWindow()
if err != nil {
errch <- err
win.SetDriver(w)
res <- struct{}{}
if err := w.init(); err != nil {
w.ProcessEvent(DestroyEvent{Err: err})
return
}
errch <- nil
w.w = win
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()
win.SetDriver(w)
w.Configure(options)
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
@@ -865,48 +979,41 @@ func newWindow(win *callbacks, options []Option) error {
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
// makeKeyAndOrderFront assumes ownership of our window reference.
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() error {
view := C.gio_createView()
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))
w := &window{
view: view,
scale: scale,
redraw: make(chan struct{}, 1),
}
dl, err := NewDisplayLink(func() {
w.scale = scale
dl, err := newDisplayLink(func() {
select {
case w.redraw <- struct{}{}:
default:
return
}
w.runOnMain(func() {
C.setNeedsDisplay(w.view)
if w.visible {
C.setNeedsDisplay(w.view)
}
})
})
w.displayLink = dl
if err != nil {
C.CFRelease(view)
return nil, err
return err
}
insertView(view, w)
return w, nil
C.gio_viewSetHandle(view, C.uintptr_t(cgo.NewHandle(w)))
w.view = view
return nil
}
func osMain() {
C.gio_main()
}
func convertKey(k rune) (string, bool) {
var n string
func convertKey(k rune) (key.Name, bool) {
var n key.Name
switch k {
case 0x1b:
n = key.NameEscape
@@ -967,7 +1074,7 @@ func convertKey(k rune) (string, bool) {
if !unicode.IsPrint(k) {
return "", false
}
n = string(k)
n = key.Name(k)
}
return n, true
}
@@ -989,4 +1096,14 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
return kmods
}
func (_ ViewEvent) ImplementsEvent() {}
func (w *window) wakeup() {
runOnMain(func() {
w.loop.Wakeup()
if w != mainThreadWindow {
w.loop.FlushEvents()
}
})
}
func (AppKitViewEvent) implementsViewEvent() {}
func (AppKitViewEvent) ImplementsEvent() {}
+326 -69
View File
@@ -14,40 +14,50 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioWindowDelegate : NSObject<NSWindowDelegate>
@end
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@property uintptr_t handle;
@end
@implementation GioWindowDelegate
- (void)windowWillMiniaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onHide((__bridge CFTypeRef)window.contentView);
GioView *view = (GioView *)window.contentView;
gio_onHide(view.handle);
}
- (void)windowDidDeminiaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onShow((__bridge CFTypeRef)window.contentView);
GioView *view = (GioView *)window.contentView;
gio_onShow(view.handle);
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onFullscreen((__bridge CFTypeRef)window.contentView);
GioView *view = (GioView *)window.contentView;
gio_onFullscreen(view.handle);
}
- (void)windowWillExitFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onWindowed((__bridge CFTypeRef)window.contentView);
GioView *view = (GioView *)window.contentView;
gio_onWindowed(view.handle);
}
- (void)windowDidChangeScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
CGDirectDisplayID dispID = [[[window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
CFTypeRef view = (__bridge CFTypeRef)window.contentView;
gio_onChangeScreen(view, dispID);
GioView *view = (GioView *)window.contentView;
gio_onChangeScreen(view.handle, dispID);
}
- (void)windowDidBecomeKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onFocus((__bridge CFTypeRef)window.contentView, 1);
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 1);
}
- (void)windowDidResignKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
gio_onFocus((__bridge CFTypeRef)window.contentView, 0);
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 0);
}
@end
static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFloat dy) {
NSPoint p = [view convertPoint:[event locationInWindow] fromView:nil];
if (!event.hasPreciseScrollingDeltas) {
// dx and dy are in rows and columns.
@@ -56,10 +66,213 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
}
// Origin is in the lower left corner. Convert to upper left.
CGFloat height = view.bounds.size.height;
gio_onMouse((__bridge CFTypeRef)view, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
}
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@interface GioApplication: NSApplication
@end
// Variables for tracking resizes.
static struct {
NSPoint dir;
NSEvent *lastMouseDown;
NSPoint off;
} resizeState = {};
static NSBitmapImageRep *nsImageBitmap(NSImage *img) {
NSArray<NSImageRep *> *reps = img.representations;
if ([reps count] == 0) {
return nil;
}
NSImageRep *rep = reps[0];
if (![rep isKindOfClass:[NSBitmapImageRep class]]) {
return nil;
}
return (NSBitmapImageRep *)rep;
}
static NSCursor *lookupPrivateNSCursor(SEL name) {
if (![NSCursor respondsToSelector:name]) {
return nil;
}
id obj = [NSCursor performSelector:name];
if (![obj isKindOfClass:[NSCursor class]]) {
return nil;
}
return (NSCursor *)obj;
}
static BOOL isEqualNSCursor(NSCursor *c1, SEL name2) {
NSCursor *c2 = lookupPrivateNSCursor(name2);
if (c2 == nil || !NSEqualPoints(c1.hotSpot, c2.hotSpot)) {
return NO;
}
NSImage *img1 = c1.image;
NSImage *img2 = c2.image;
if (!NSEqualSizes(img1.size, img2.size)) {
return NO;
}
NSBitmapImageRep *bit1 = nsImageBitmap(img1);
NSBitmapImageRep *bit2 = nsImageBitmap(img2);
if (bit1 == nil || bit2 == nil) {
return NO;
}
NSInteger n1 = bit1.numberOfPlanes*bit1.bytesPerPlane;
NSInteger n2 = bit1.numberOfPlanes*bit1.bytesPerPlane;
if (n1 != n2) {
return NO;
}
if (memcmp(bit1.bitmapData, bit2.bitmapData, n1) != 0) {
return NO;
}
return YES;
}
@implementation GioApplication
- (NSEvent *)nextEventMatchingMask:(NSEventMask)mask
untilDate:(NSDate *)expiration
inMode:(NSRunLoopMode)mode
dequeue:(BOOL)deqFlag {
if ([mode isEqualToString:NSEventTrackingRunLoopMode]) {
NSEvent *l = resizeState.lastMouseDown;
if (l != nil) {
//lastMouseDown = nil;
NSCursor *cur = [NSCursor currentSystemCursor];
NSPoint dir = {};
NSPoint off = {};
NSSize wsz = [l window].frame.size;
NSPoint center = NSMakePoint(wsz.width/2, wsz.height/2);
NSPoint p = [l locationInWindow];
if (p.x >= center.x) {
dir.x = 1;
off.x = p.x - wsz.width;
} else {
dir.x = -1;
off.x = p.x;
}
if (p.y >= center.y) {
dir.y = 1;
off.y = p.y - wsz.height;
} else {
dir.y = -1;
off.y = p.y;
}
// The button down coordinate distinguish the four quadrants. Use the
// cursor image to determine the precise direction.
SEL nw = @selector(_windowResizeNorthWestCursor);
SEL n = @selector(_windowResizeNorthCursor);
SEL ne = @selector(_windowResizeNorthEastCursor);
SEL e = @selector(_windowResizeEastCursor);
SEL se = @selector(_windowResizeSouthEastCursor);
SEL s = @selector(_windowResizeSouthCursor);
SEL sw = @selector(_windowResizeSouthWestCursor);
SEL w = @selector(_windowResizeWestCursor);
SEL ns = @selector(_windowResizeNorthSouthCursor);
SEL ew = @selector(_windowResizeEastWestCursor);
SEL nwse = @selector(_windowResizeNorthWestSouthEastCursor);
SEL nesw = @selector(_windowResizeNorthEastSouthWestCursor);
BOOL match = YES;
if (dir.x != 0 && (isEqualNSCursor(cur, ew) || isEqualNSCursor(cur, w) || isEqualNSCursor(cur, e))) {
dir.y = 0;
}
if (dir.y != 0 && (isEqualNSCursor(cur, ns) || isEqualNSCursor(cur, s) || isEqualNSCursor(cur, n))) {
dir.x = 0;
}
// If none of the cursors matched, we may deduce that the resize
// direction is one of the corners. However, to ensure that at least
// one cursor matches, check the corner cursors.
if (dir.x == 1 && dir.y == 1) {
if (!isEqualNSCursor(cur, nesw) && !isEqualNSCursor(cur, sw)) {
dir = NSZeroPoint;
}
} else if (dir.x == 1 && dir.y == -1) {
if (!isEqualNSCursor(cur, nwse) && !isEqualNSCursor(cur, nw)) {
dir = NSZeroPoint;
}
} else if (dir.x == -1 && dir.y == 1) {
if (!isEqualNSCursor(cur, nwse) && !isEqualNSCursor(cur, se)) {
dir = NSZeroPoint;
}
} else if (dir.x == -1 && dir.y == -1) {
if (!isEqualNSCursor(cur, nesw) && !isEqualNSCursor(cur, ne)) {
dir = NSZeroPoint;
}
}
if (!NSEqualPoints(dir, NSZeroPoint)) {
NSEvent *cancel = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp
location:l.locationInWindow
modifierFlags:l.modifierFlags
timestamp:l.timestamp
windowNumber:l.windowNumber
context:l.context
eventNumber:l.eventNumber
clickCount:l.clickCount
pressure:l.pressure];
resizeState.off = off;
resizeState.dir = dir;
return cancel;
}
}
}
return [super nextEventMatchingMask:mask untilDate:expiration inMode:mode dequeue:deqFlag];
}
@end
@interface GioWindow: NSWindow
@end
@implementation GioWindow
- (void)sendEvent:(NSEvent *)evt {
if (evt.type == NSEventTypeLeftMouseDown) {
resizeState.lastMouseDown = evt;
}
NSPoint dir = resizeState.dir;
if (NSEqualPoints(dir, NSZeroPoint)) {
[super sendEvent:evt];
return;
}
switch (evt.type) {
default:
return;
case NSEventTypeLeftMouseUp:
resizeState.dir = NSZeroPoint;
resizeState.lastMouseDown = nil;
return;
case NSEventTypeLeftMouseDragged:
// Ok to proceed.
break;
}
NSPoint loc = evt.locationInWindow;
NSPoint off = resizeState.off;
loc.x -= off.x;
loc.y -= off.y;
NSRect frame = [self frame];
NSSize min = [self minSize];
NSSize max = [self maxSize];
CGFloat width = frame.size.width;
if (dir.x > 0) {
width = loc.x;
} else if (dir.x < 0) {
width -= loc.x;
}
width = MIN(max.width, MAX(min.width, width));
if (dir.x < 0) {
frame.origin.x += frame.size.width - width;
}
frame.size.width = width;
CGFloat height = frame.size.height;
if (dir.y > 0) {
height = loc.y;
} else if (dir.y < 0) {
height -= loc.y;
}
height = MIN(max.height, MAX(min.height, height));
if (dir.y < 0) {
frame.origin.y += frame.size.height - height;
}
frame.size.height = height;
[self setFrame:frame display:YES animate:NO];
}
@end
@implementation GioView
@@ -70,11 +283,11 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
// drawRect is called when OpenGL is used, displayLayer otherwise.
// Don't know why.
- (void)drawRect:(NSRect)r {
gio_onDraw((__bridge CFTypeRef)self);
gio_onDraw(self.handle);
}
- (void)displayLayer:(CALayer *)layer {
layer.contentsScale = self.window.backingScaleFactor;
gio_onDraw((__bridge CFTypeRef)self);
gio_onDraw(self.handle);
}
- (CALayer *)makeBackingLayer {
CALayer *layer = gio_layerFactory();
@@ -82,9 +295,7 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
return layer;
}
- (void)viewDidMoveToWindow {
if (self.window == nil) {
gio_onClose((__bridge CFTypeRef)self);
}
gio_onAttached(self.handle, self.window != nil ? 1 : 0);
}
- (void)mouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
@@ -92,24 +303,30 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
- (void)mouseUp:(NSEvent *)event {
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 {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
}
- (void)rightMouseUp:(NSEvent *)event {
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 {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
- (void)mouseDragged:(NSEvent *)event {
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 {
CGFloat dx = -event.scrollingDeltaX;
CGFloat dy = -event.scrollingDeltaY;
@@ -118,32 +335,31 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
- (void)keyDown:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
}
- (void)keyUp:(NSEvent *)event {
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys((__bridge CFTypeRef)self, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
}
- (void)insertText:(id)string {
gio_onText((__bridge CFTypeRef)self, (__bridge CFTypeRef)string);
gio_onText(self.handle, (__bridge CFTypeRef)string);
}
- (void)doCommandBySelector:(SEL)sel {
// Don't pass commands up the responder chain.
// They will end up in a beep.
}
- (BOOL)hasMarkedText {
int res = gio_hasMarkedText((__bridge CFTypeRef)self);
int res = gio_hasMarkedText(self.handle);
return res ? YES : NO;
}
- (NSRange)markedRange {
return gio_markedRange((__bridge CFTypeRef)self);
return gio_markedRange(self.handle);
}
- (NSRange)selectedRange {
return gio_selectedRange((__bridge CFTypeRef)self);
return gio_selectedRange(self.handle);
}
- (void)unmarkText {
gio_unmarkText((__bridge CFTypeRef)self);
gio_unmarkText(self.handle);
}
- (void)setMarkedText:(id)string
selectedRange:(NSRange)selRange
@@ -155,14 +371,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
} else {
str = string;
}
gio_setMarkedText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, selRange, replaceRange);
gio_setMarkedText(self.handle, (__bridge CFTypeRef)str, selRange, replaceRange);
}
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText {
return nil;
}
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range
actualRange:(NSRangePointer)actualRange {
NSString *str = CFBridgingRelease(gio_substringForProposedRange((__bridge CFTypeRef)self, range, actualRange));
NSString *str = CFBridgingRelease(gio_substringForProposedRange(self.handle, range, actualRange));
return [[NSAttributedString alloc] initWithString:str attributes:nil];
}
- (void)insertText:(id)string
@@ -174,17 +390,26 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
} else {
str = string;
}
gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange);
gio_insertText(self.handle, (__bridge CFTypeRef)str, replaceRange);
}
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
return gio_characterIndexForPoint((__bridge CFTypeRef)self, p);
return gio_characterIndexForPoint(self.handle, p);
}
- (NSRect)firstRectForCharacterRange:(NSRange)rng
actualRange:(NSRangePointer)actual {
NSRect r = gio_firstRectForCharacterRange((__bridge CFTypeRef)self, rng, actual);
NSRect r = gio_firstRectForCharacterRange(self.handle, rng, actual);
r = [self convertRect:r toView:nil];
return [[self window] convertRectToScreen:r];
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onShow(self.handle);
}
- (void)applicationDidHide:(NSNotification *)notification {
gio_onHide(self.handle);
}
- (void)dealloc {
gio_onDestroy(self.handle);
}
@end
// Delegates are weakly referenced from their peers. Nothing
@@ -193,14 +418,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
static GioWindowDelegate *globalWindowDel;
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;
}
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
CFTypeRef gio_createDisplayLink(void) {
CVDisplayLinkRef dl;
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, (void *)(handle));
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil);
return dl;
}
@@ -234,15 +459,12 @@ void gio_showCursor() {
// some cursors are not public, this tries to use a private cursor
// and uses fallback when the use of private cursor fails.
void gio_trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
if ([NSCursor respondsToSelector:cursorName]) {
id object = [NSCursor performSelector:cursorName];
if ([object isKindOfClass:[NSCursor class]]) {
[(NSCursor*)object set];
return;
}
static void trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
NSCursor *cur = lookupPrivateNSCursor(cursorName);
if (cur == nil) {
cur = fallback;
}
[fallback set];
[cur set];
}
void gio_setCursor(NSUInteger curID) {
@@ -266,7 +488,7 @@ void gio_setCursor(NSUInteger curID) {
break;
case 6: // pointer.CursorAllScroll
// For some reason, using _moveCursor fails on Monterey.
// gio_trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
// trySetPrivateCursor(@selector(_moveCursor), NSCursor.arrowCursor);
[NSCursor.arrowCursor set];
break;
case 7: // pointer.CursorColResize
@@ -276,33 +498,31 @@ void gio_setCursor(NSUInteger curID) {
[NSCursor.resizeUpDownCursor set];
break;
case 9: // pointer.CursorGrab
// [NSCursor.openHandCursor set];
gio_trySetPrivateCursor(@selector(openHandCursor), NSCursor.arrowCursor);
[NSCursor.openHandCursor set];
break;
case 10: // pointer.CursorGrabbing
// [NSCursor.closedHandCursor set];
gio_trySetPrivateCursor(@selector(closedHandCursor), NSCursor.arrowCursor);
[NSCursor.closedHandCursor set];
break;
case 11: // pointer.CursorNotAllowed
[NSCursor.operationNotAllowedCursor set];
break;
case 12: // pointer.CursorWait
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break;
case 13: // pointer.CursorProgress
gio_trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
trySetPrivateCursor(@selector(busyButClickableCursor), NSCursor.arrowCursor);
break;
case 14: // pointer.CursorNorthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeNorthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 15: // pointer.CursorNorthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeNorthEastCursor), NSCursor.resizeUpDownCursor);
break;
case 16: // pointer.CursorSouthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeSouthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 17: // pointer.CursorSouthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeSouthEastCursor), NSCursor.resizeUpDownCursor);
break;
case 18: // pointer.CursorNorthSouthResize
[NSCursor.resizeUpDownCursor set];
@@ -323,10 +543,10 @@ void gio_setCursor(NSUInteger curID) {
[NSCursor.resizeDownCursor set];
break;
case 24: // pointer.CursorNorthEastSouthWestResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeNorthEastSouthWestCursor), NSCursor.resizeUpDownCursor);
break;
case 25: // pointer.CursorNorthWestSouthEastResize
gio_trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
trySetPrivateCursor(@selector(_windowResizeNorthWestSouthEastCursor), NSCursor.resizeUpDownCursor);
break;
default:
[NSCursor.arrowCursor set];
@@ -343,7 +563,7 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
NSMiniaturizableWindowMask |
NSClosableWindowMask;
NSWindow* window = [[NSWindow alloc] initWithContentRect:rect
GioWindow* window = [[GioWindow alloc] initWithContentRect:rect
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:NO];
@@ -368,27 +588,48 @@ CFTypeRef gio_createView(void) {
GioView* view = [[GioView alloc] initWithFrame:frame];
view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
[[NSNotificationCenter defaultCenter] addObserver:view
selector:@selector(applicationWillUnhide:)
name:NSApplicationWillUnhideNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:view
selector:@selector(applicationDidHide:)
name:NSApplicationDidHideNotification
object:nil];
return CFBridgingRetain(view);
}
}
void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
@autoreleasepool {
GioView *v = (__bridge GioView *)viewRef;
v.handle = handle;
}
}
@implementation GioAppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES];
gio_onFinishLaunching();
}
- (void)applicationDidHide:(NSNotification *)aNotification {
gio_onAppHide();
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onAppShow();
// Force the [NSApp run] call to return.
[NSApp stop:nil];
NSEvent *dummy = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSZeroPoint
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:dummy atStart:YES];
}
@end
void gio_main() {
void gio_initApp() {
@autoreleasepool {
[NSApplication sharedApplication];
[GioApplication sharedApplication];
GioAppDelegate *del = [[GioAppDelegate alloc] init];
[NSApp setDelegate:del];
@@ -410,6 +651,22 @@ void gio_main() {
globalWindowDel = [[GioWindowDelegate alloc] init];
// Runs until stopped by applicationDidFinishLaunching.
[NSApp run];
}
}
void gio_wakeupMainThread(void) {
@autoreleasepool {
NSEvent *dummy = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSZeroPoint
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:dummy atStart:YES];
}
}
+29 -16
View File
@@ -9,16 +9,10 @@ import (
"errors"
"unsafe"
"gioui.org/io/event"
"gioui.org/io/pointer"
)
// ViewEvent provides handles to the underlying window objects for the
// current display protocol.
type ViewEvent interface {
implementsViewEvent()
ImplementsEvent()
}
type X11ViewEvent struct {
// Display is a pointer to the X11 Display created by XOpenDisplay.
Display unsafe.Pointer
@@ -39,17 +33,13 @@ type WaylandViewEvent struct {
func (WaylandViewEvent) implementsViewEvent() {}
func (WaylandViewEvent) ImplementsEvent() {}
func osMain() {
select {}
}
type windowDriver func(*callbacks, []Option) error
// Instead of creating files with build tags for each combination of wayland +/- x11
// let each driver initialize these variables with their own version of createWindow.
var wlDriver, x11Driver windowDriver
func newWindow(window *callbacks, options []Option) error {
func newWindow(window *callbacks, options []Option) {
var errFirst error
for _, d := range []windowDriver{wlDriver, x11Driver} {
if d == nil {
@@ -57,16 +47,39 @@ func newWindow(window *callbacks, options []Option) error {
}
err := d(window, options)
if err == nil {
return nil
return
}
if errFirst == nil {
errFirst = err
}
}
if errFirst != nil {
return errFirst
window.SetDriver(&dummyDriver{
win: window,
wakeups: make(chan event.Event, 1),
})
if errFirst == nil {
errFirst = errors.New("app: no window driver available")
}
window.ProcessEvent(DestroyEvent{Err: errFirst})
}
type dummyDriver struct {
win *callbacks
wakeups chan event.Event
}
func (d *dummyDriver) Event() event.Event {
if e, ok := d.win.nextEvent(); ok {
return e
}
return <-d.wakeups
}
func (d *dummyDriver) Invalidate() {
select {
case d.wakeups <- wakeupEvent{}:
default:
}
return errors.New("app: no window driver available")
}
// xCursor contains mapping from pointer.Cursor to XCursor.
+186 -123
View File
@@ -15,6 +15,7 @@ import (
"math"
"os"
"os/exec"
"runtime"
"strconv"
"sync"
"time"
@@ -25,10 +26,12 @@ import (
"gioui.org/app/internal/xkb"
"gioui.org/f32"
"gioui.org/internal/fling"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
)
@@ -97,7 +100,9 @@ type wlDisplay struct {
read, write int
}
repeat repeatState
repeat repeatState
poller poller
readClipClose chan struct{}
}
type wlSeat struct {
@@ -137,7 +142,7 @@ type repeatState struct {
delay time.Duration
key uint32
win *callbacks
win *window
stopC chan struct{}
start time.Duration
@@ -194,12 +199,10 @@ type window struct {
dir f32.Point
}
stage system.Stage
dead bool
configured bool
lastFrameCallback *C.struct_wl_callback
animating bool
redraw bool
// The most recent configure serial waiting to be ack'ed.
serial C.uint32_t
scale int
@@ -209,9 +212,13 @@ type window struct {
wsize image.Point // window config size before going fullscreen or maximized
inCompositor bool // window is moving or being resized
clipReads chan clipboard.Event
clipReads chan transfer.DataEvent
wakeups chan struct{}
// invMu avoids the race between the destruction of disp and
// Invalidate waking it up.
invMu sync.Mutex
}
type poller struct {
@@ -260,25 +267,17 @@ func newWLWindow(callbacks *callbacks, options []Option) error {
return err
}
w.w = callbacks
go func() {
defer d.destroy()
defer w.destroy()
w.w.SetDriver(w)
w.w.SetDriver(w)
// Finish and commit setup from createNativeWindow.
w.Configure(options)
w.draw(true)
C.wl_surface_commit(w.surf)
// Finish and commit setup from createNativeWindow.
w.Configure(options)
C.wl_surface_commit(w.surf)
w.w.Event(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
err := w.loop()
w.w.Event(WaylandViewEvent{})
w.w.Event(system.DestroyEvent{Err: err})
}()
w.ProcessEvent(WaylandViewEvent{
Display: unsafe.Pointer(w.display()),
Surface: unsafe.Pointer(w.surf),
})
return nil
}
@@ -354,7 +353,7 @@ func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) {
ppdp: ppdp,
ppsp: ppdp,
wakeups: make(chan struct{}, 1),
clipReads: make(chan clipboard.Event, 1),
clipReads: make(chan transfer.DataEvent, 1),
}
w.surf = C.wl_compositor_create_surface(d.compositor)
if w.surf == nil {
@@ -549,15 +548,15 @@ func gio_onSeatName(data unsafe.Pointer, seat *C.struct_wl_seat, name *C.char) {
func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface, serial C.uint32_t) {
w := callbackLoad(data).(*window)
w.serial = serial
w.redraw = true
C.xdg_surface_ack_configure(wmSurf, serial)
w.setStage(system.StageRunning)
w.configured = true
w.draw(true)
}
//export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := callbackLoad(data).(*window)
w.dead = true
w.close(nil)
}
//export gio_onToplevelConfigure
@@ -586,8 +585,8 @@ func gio_onToplevelDecorationConfigure(data unsafe.Pointer, deco *C.struct_zxdg_
} else {
w.size.Y += int(w.config.decoHeight)
}
w.w.Event(ConfigEvent{Config: w.config})
w.redraw = true
w.ProcessEvent(ConfigEvent{Config: w.config})
w.draw(true)
}
}
@@ -645,7 +644,7 @@ func gio_onSurfaceEnter(data unsafe.Pointer, surf *C.struct_wl_surface, output *
if w.config.Mode == Minimized {
// Minimized window got brought back up: it is no longer so.
w.config.Mode = Windowed
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
}
@@ -790,8 +789,8 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
Type: pointer.Press,
w.ProcessEvent(pointer.Event{
Kind: pointer.Press,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
@@ -806,8 +805,8 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui
s.serial = serial
w := s.touchFoci[id]
delete(s.touchFoci, id)
w.w.Event(pointer.Event{
Type: pointer.Release,
w.ProcessEvent(pointer.Event{
Kind: pointer.Release,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
@@ -824,8 +823,8 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
Type: pointer.Move,
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Position: w.lastTouch,
Source: pointer.Touch,
PointerID: pointer.ID(id),
@@ -843,8 +842,8 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
s := callbackLoad(data).(*wlSeat)
for id, w := range s.touchFoci {
delete(s.touchFoci, id)
w.w.Event(pointer.Event{
Type: pointer.Cancel,
w.ProcessEvent(pointer.Event{
Kind: pointer.Cancel,
Source: pointer.Touch,
})
}
@@ -869,7 +868,7 @@ func gio_onPointerLeave(data unsafe.Pointer, p *C.struct_wl_pointer, serial C.ui
s.serial = serial
if w.inCompositor {
w.inCompositor = false
w.w.Event(pointer.Event{Type: pointer.Cancel})
w.ProcessEvent(pointer.Event{Kind: pointer.Cancel})
}
}
@@ -917,21 +916,21 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
}
}
}
var typ pointer.Type
var kind pointer.Kind
switch state {
case 0:
w.pointerBtns &^= btn
typ = pointer.Release
kind = pointer.Release
// Move or resize gestures no longer applies.
w.inCompositor = false
case 1:
w.pointerBtns |= btn
typ = pointer.Press
kind = pointer.Press
}
w.flushScroll()
w.resetFling()
w.w.Event(pointer.Event{
Type: typ,
w.ProcessEvent(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: w.lastPos,
@@ -1018,23 +1017,34 @@ func gio_onPointerAxisDiscrete(data unsafe.Pointer, p *C.struct_wl_pointer, axis
}
func (w *window) ReadClipboard() {
if w.disp.readClipClose != nil {
return
}
w.disp.readClipClose = make(chan struct{})
r, err := w.disp.readClipboard()
// Send empty responses on unavailable clipboards or errors.
if r == nil || err != nil {
w.w.Event(clipboard.Event{})
return
}
// Don't let slow clipboard transfers block event loop.
go func() {
defer r.Close()
data, _ := io.ReadAll(r)
w.clipReads <- clipboard.Event{Text: string(data)}
w.Wakeup()
e := transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(bytes.NewReader(data))
},
}
select {
case w.clipReads <- e:
w.disp.wakeup()
case <-w.disp.readClipClose:
}
}()
}
func (w *window) WriteClipboard(s string) {
w.disp.writeClipboard([]byte(s))
func (w *window) WriteClipboard(mime string, s []byte) {
w.disp.writeClipboard(s)
}
func (w *window) Configure(options []Option) {
@@ -1092,8 +1102,7 @@ func (w *window) Configure(options []Option) {
w.config.MaxSize = cnf.MaxSize
w.setWindowConstraints()
}
w.w.Event(ConfigEvent{Config: w.config})
w.redraw = true
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *window) setWindowConstraints() {
@@ -1130,7 +1139,7 @@ func (w *window) Perform(actions system.Action) {
walkActions(actions, func(action system.Action) {
switch action {
case system.ActionClose:
w.dead = true
w.close(nil)
}
})
}
@@ -1213,7 +1222,8 @@ func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
w := callbackLoad(unsafe.Pointer(surf)).(*window)
s.keyboardFocus = w
s.disp.repeat.Stop(0)
w.w.Event(key.FocusEvent{Focus: true})
w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onKeyboardLeave
@@ -1222,7 +1232,8 @@ func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
s.serial = serial
s.disp.repeat.Stop(0)
w := s.keyboardFocus
w.w.Event(key.FocusEvent{Focus: false})
w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onKeyboardKey
@@ -1240,7 +1251,7 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.w.Event(e)
w.ProcessEvent(e)
}
}
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
@@ -1275,7 +1286,7 @@ func (r *repeatState) Start(w *window, keyCode uint32, t time.Duration) {
r.now = 0
r.stopC = stopC
r.key = keyCode
r.win = w.w
r.win = w
rate, delay := r.rate, r.delay
go func() {
timer := time.NewTimer(delay)
@@ -1333,9 +1344,9 @@ func (r *repeatState) Repeat(d *wlDisplay) {
for _, e := range d.xkb.DispatchKey(r.key, key.Press) {
if ee, ok := e.(key.EditEvent); ok {
// There's no support for IME yet.
r.win.EditorInsert(ee.Text)
r.win.w.EditorInsert(ee.Text)
} else {
r.win.Event(e)
r.win.ProcessEvent(e)
}
}
r.last += delay
@@ -1348,28 +1359,76 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
w := callbackLoad(data).(*window)
if w.lastFrameCallback == callback {
w.lastFrameCallback = nil
w.draw(false)
}
}
func (w *window) loop() error {
var p poller
for {
if err := w.disp.dispatch(&p); err != nil {
return err
}
select {
case e := <-w.clipReads:
w.w.Event(e)
case <-w.wakeups:
w.w.Event(wakeupEvent{})
default:
}
if w.dead {
break
}
w.draw()
func (w *window) close(err error) {
w.ProcessEvent(WaylandViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
}
func (w *window) dispatch() {
if w.disp == nil {
<-w.wakeups
w.w.Invalidate()
return
}
return nil
if err := w.disp.dispatch(); err != nil {
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
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
w.invMu.Lock()
w.disp.destroy()
w.disp = nil
w.invMu.Unlock()
}
return evt
}
}
func (w *window) Invalidate() {
select {
case w.wakeups <- struct{}{}:
default:
return
}
w.invMu.Lock()
defer w.invMu.Unlock()
if w.disp != nil {
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
@@ -1385,13 +1444,21 @@ func (d *wlDisplay) bindDataDevice() {
}
}
func (d *wlDisplay) dispatch(p *poller) error {
func (d *wlDisplay) dispatch() error {
// wl_display_prepare_read records the current thread for
// use in wl_display_read_events or wl_display_cancel_events.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
dispfd := C.wl_display_get_fd(d.disp)
// Poll for events and notifications.
pollfds := append(p.pollfds[:0],
pollfds := append(d.poller.pollfds[:0],
syscall.PollFd{Fd: int32(dispfd), Events: syscall.POLLIN | syscall.POLLERR},
syscall.PollFd{Fd: int32(d.notify.read), Events: syscall.POLLIN | syscall.POLLERR},
)
for C.wl_display_prepare_read(d.disp) != 0 {
C.wl_display_dispatch_pending(d.disp)
}
dispFd := &pollfds[0]
if ret, err := C.wl_display_flush(d.disp); ret < 0 {
if err != syscall.EAGAIN {
@@ -1402,11 +1469,25 @@ func (d *wlDisplay) dispatch(p *poller) error {
dispFd.Events |= syscall.POLLOUT
}
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
C.wl_display_cancel_read(d.disp)
return fmt.Errorf("wayland: poll failed: %v", err)
}
if dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0 {
C.wl_display_cancel_read(d.disp)
return errors.New("wayland: display file descriptor gone")
}
// Handle events.
if dispFd.Revents&syscall.POLLIN != 0 {
if ret, err := C.wl_display_read_events(d.disp); ret < 0 {
return fmt.Errorf("wayland: wl_display_read_events failed: %v", err)
}
C.wl_display_dispatch_pending(d.disp)
} else {
C.wl_display_cancel_read(d.disp)
}
// Clear notifications.
for {
_, err := syscall.Read(d.notify.read, p.buf[:])
_, err := syscall.Read(d.notify.read, d.poller.buf[:])
if err == syscall.EAGAIN {
break
}
@@ -1414,29 +1495,15 @@ func (d *wlDisplay) dispatch(p *poller) error {
return fmt.Errorf("wayland: read from notify pipe failed: %v", err)
}
}
// Handle events
switch {
case dispFd.Revents&syscall.POLLIN != 0:
if ret, err := C.wl_display_dispatch(d.disp); ret < 0 {
return fmt.Errorf("wayland: wl_display_dispatch failed: %v", err)
}
case dispFd.Revents&(syscall.POLLERR|syscall.POLLHUP) != 0:
return errors.New("wayland: display file descriptor gone")
}
d.repeat.Repeat(d)
return nil
}
func (w *window) Wakeup() {
select {
case w.wakeups <- struct{}{}:
default:
}
w.disp.wakeup()
}
func (w *window) SetAnimating(anim bool) {
w.animating = anim
if anim {
w.draw(false)
}
}
// Wakeup wakes up the event loop through the notification pipe.
@@ -1572,8 +1639,8 @@ func (w *window) flushScroll() {
if total == (f32.Point{}) {
return
}
w.w.Event(pointer.Event{
Type: pointer.Scroll,
w.ProcessEvent(pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: w.lastPos,
@@ -1595,8 +1662,8 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
X: fromFixed(x) * float32(w.scale),
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
Type: pointer.Move,
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Position: w.lastPos,
Buttons: w.pointerBtns,
Source: pointer.Mouse,
@@ -1616,7 +1683,8 @@ func (w *window) systemGesture() (*C.struct_wl_cursor, C.uint32_t) {
if w.config.Mode != Windowed || w.config.Decorated {
return nil, 0
}
border := w.w.w.metric.Dp(3)
_, cfg := w.getConfig()
border := cfg.Dp(3)
x, y, size := int(w.lastPos.X), int(w.lastPos.Y), w.config.Size
north := y <= border
south := y >= size.Y-border
@@ -1670,13 +1738,10 @@ func (w *window) updateOutputs() {
if found && scale != w.scale {
w.scale = scale
C.wl_surface_set_buffer_scale(w.surf, C.int32_t(w.scale))
w.redraw = true
w.draw(true)
}
if !found {
w.setStage(system.StagePaused)
} else {
w.setStage(system.StageRunning)
w.redraw = true
if found {
w.draw(true)
}
}
@@ -1688,7 +1753,10 @@ func (w *window) getConfig() (image.Point, unit.Metric) {
}
}
func (w *window) draw() {
func (w *window) draw(sync bool) {
if !w.configured {
return
}
w.flushScroll()
size, cfg := w.getConfig()
if cfg == (unit.Metric{}) {
@@ -1696,11 +1764,9 @@ func (w *window) draw() {
}
if size != w.config.Size {
w.config.Size = size
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
anim := w.animating || w.fling.anim.Active()
sync := w.redraw
w.redraw = false
// Draw animation only when not waiting for frame callback.
redrawAnim := anim && w.lastFrameCallback == nil
if !redrawAnim && !sync {
@@ -1711,8 +1777,8 @@ func (w *window) draw() {
// Use the surface as listener data for gio_onFrameDone.
C.wl_callback_add_listener(w.lastFrameCallback, &C.gio_callback_listener, unsafe.Pointer(w.surf))
}
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: cfg,
@@ -1721,14 +1787,6 @@ func (w *window) draw() {
})
}
func (w *window) setStage(s system.Stage) {
if s == w.stage {
return
}
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
}
func (w *window) display() *C.struct_wl_display {
return w.disp.disp
}
@@ -1820,6 +1878,10 @@ func newWLDisplay() (*wlDisplay, error) {
}
func (d *wlDisplay) destroy() {
if d.readClipClose != nil {
close(d.readClipClose)
d.readClipClose = nil
}
if d.notify.write != 0 {
syscall.Close(d.notify.write)
d.notify.write = 0
@@ -1861,6 +1923,7 @@ func (d *wlDisplay) destroy() {
if d.disp != nil {
C.wl_display_disconnect(d.disp)
callbackDelete(unsafe.Pointer(d.disp))
d.disp = nil
}
}
+166 -132
View File
@@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"image"
"io"
"runtime"
"sort"
"strings"
@@ -18,30 +19,26 @@ import (
syscall "golang.org/x/sys/windows"
"gioui.org/app/internal/windows"
"gioui.org/op"
"gioui.org/unit"
gowindows "golang.org/x/sys/windows"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
)
type ViewEvent struct {
type Win32ViewEvent struct {
HWND uintptr
}
type winDeltas struct {
width int32
height int32
}
type window struct {
hwnd syscall.Handle
hdc syscall.Handle
w *callbacks
stage system.Stage
pointerBtns pointer.Buttons
// cursorIn tracks whether the cursor was inside the window according
@@ -53,11 +50,13 @@ type window struct {
placement *windows.WindowPlacement
animating bool
focused bool
deltas winDeltas
borderSize image.Point
config Config
loop *eventLoop
// invMu avoids the race between destroying the window and Invalidate.
invMu sync.Mutex
}
const _WM_WAKEUP = windows.WM_USER + iota
@@ -86,40 +85,38 @@ var resources struct {
cursor syscall.Handle
}
func osMain() {
select {}
}
func newWindow(window *callbacks, options []Option) error {
cerr := make(chan error)
func newWindow(win *callbacks, options []Option) {
done := make(chan struct{})
go func() {
// GetMessage and PeekMessage can filter on a window HWND, but
// then thread-specific messages such as WM_QUIT are ignored.
// Instead lock the thread so window messages arrive through
// unfiltered GetMessage calls.
runtime.LockOSThread()
w, 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 {
cerr <- err
w.ProcessEvent(DestroyEvent{Err: err})
return
}
cerr <- nil
winMap.Store(w.hwnd, w)
defer winMap.Delete(w.hwnd)
w.w = window
w.w.SetDriver(w)
w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)})
w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)})
w.Configure(options)
windows.SetForegroundWindow(w.hwnd)
windows.SetFocus(w.hwnd)
// Since the window class for the cursor is null,
// set it here to show the cursor.
w.SetCursor(pointer.CursorDefault)
if err := w.loop(); err != nil {
panic(err)
}
w.runLoop()
}()
return <-cerr
<-done
}
// initResources initializes the resources global.
@@ -154,13 +151,13 @@ func initResources() error {
const dwExStyle = windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE
func createNativeWindow() (*window, error) {
func (w *window) init() error {
var resErr error
resources.once.Do(func() {
resErr = initResources()
})
if resErr != nil {
return nil, resErr
return resErr
}
const dwStyle = windows.WS_OVERLAPPEDWINDOW
@@ -176,43 +173,32 @@ func createNativeWindow() (*window, error) {
resources.handle,
0)
if err != nil {
return nil, err
}
w := &window{
hwnd: hwnd,
return err
}
w.hdc, err = windows.GetDC(hwnd)
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.
// It reads the window style and size/position and updates w.config.
// If anything has changed it emits a ConfigEvent to notify the application.
func (w *window) update() {
r := windows.GetWindowRect(w.hwnd)
size := image.Point{
X: int(r.Right - r.Left - w.deltas.width),
Y: int(r.Bottom - r.Top - w.deltas.height),
cr := windows.GetClientRect(w.hwnd)
w.config.Size = image.Point{
X: int(cr.Right - cr.Left),
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(
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
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 {
@@ -253,7 +239,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
e.State = key.Release
}
w.w.Event(e)
w.ProcessEvent(e)
if (wParam == windows.VK_F10) && (msg == windows.WM_SYSKEYDOWN || msg == windows.WM_SYSKEYUP) {
// Reserve F10 for ourselves, and don't let it open the system menu. Other Windows programs
@@ -274,23 +260,15 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MBUTTONUP:
w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers())
case windows.WM_CANCELMODE:
w.w.Event(pointer.Event{
Type: pointer.Cancel,
w.ProcessEvent(pointer.Event{
Kind: pointer.Cancel,
})
case windows.WM_SETFOCUS:
w.focused = true
w.w.Event(key.FocusEvent{Focus: true})
w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
case windows.WM_KILLFOCUS:
w.focused = false
w.w.Event(key.FocusEvent{Focus: false})
case windows.WM_NCACTIVATE:
if w.stage >= system.StageInactive {
if wParam == windows.TRUE {
w.setStage(system.StageRunning)
} else {
w.setStage(system.StageInactive)
}
}
w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
case windows.WM_NCHITTEST:
if w.config.Decorated {
// Let the system handle it.
@@ -303,8 +281,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MOUSEMOVE:
x, y := coordsFromlParam(lParam)
p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{
Type: pointer.Move,
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Position: p,
Buttons: w.pointerBtns,
@@ -316,15 +294,39 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_MOUSEHWHEEL:
w.scrollEvent(wParam, lParam, true, getModifiers())
case windows.WM_DESTROY:
w.w.Event(ViewEvent{})
w.w.Event(system.DestroyEvent{})
w.ProcessEvent(Win32ViewEvent{})
w.ProcessEvent(DestroyEvent{})
if w.hdc != 0 {
windows.ReleaseDC(w.hdc)
w.hdc = 0
}
w.invMu.Lock()
// The system destroys the HWND for us.
w.hwnd = 0
w.invMu.Unlock()
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_PAINT:
w.draw(true)
case windows.WM_SIZE:
@@ -332,30 +334,35 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
switch wParam {
case windows.SIZE_MINIMIZED:
w.config.Mode = Minimized
w.setStage(system.StagePaused)
case windows.SIZE_MAXIMIZED:
w.config.Mode = Maximized
w.setStage(system.StageRunning)
case windows.SIZE_RESTORED:
if w.config.Mode != Fullscreen {
w.config.Mode = Windowed
}
w.setStage(system.StageRunning)
}
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 {
mm.PtMinTrackSize = windows.Point{
X: int32(p.X) + w.deltas.width,
Y: int32(p.Y) + w.deltas.height,
X: int32(p.X) + bw,
Y: int32(p.Y) + bh,
}
}
if p := w.config.MaxSize; p.X > 0 || p.Y > 0 {
mm.PtMaxTrackSize = windows.Point{
X: int32(p.X) + w.deltas.width,
Y: int32(p.Y) + w.deltas.height,
X: int32(p.X) + bw,
Y: int32(p.Y) + bh,
}
}
return 0
case windows.WM_SETCURSOR:
w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT
if w.cursorIn {
@@ -363,7 +370,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
return windows.TRUE
}
case _WM_WAKEUP:
w.w.Event(wakeupEvent{})
w.loop.Wakeup()
w.loop.FlushEvents()
case windows.WM_IME_STARTCOMPOSITION:
imc := windows.ImmGetContext(w.hwnd)
if imc == 0 {
@@ -446,23 +454,18 @@ func (w *window) hitTest(x, y int) uintptr {
if w.config.Mode == Fullscreen {
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 {
// Only windowed mode should allow resizing.
return windows.HTCLIENT
}
// Check for resize handle before system actions; otherwise it can be impossible to
// resize a custom-decorations window when the system move area is flush with the
// edge of the window.
top := y <= w.borderSize.Y
bottom := y >= w.config.Size.Y-w.borderSize.Y
left := x <= w.borderSize.X
right := x >= w.config.Size.X-w.borderSize.X
switch {
default:
fallthrough
case !top && !bottom && !left && !right:
return windows.HTCLIENT
case top && left:
return windows.HTTOPLEFT
case top && right:
@@ -480,22 +483,27 @@ func (w *window) hitTest(x, y int) uintptr {
case right:
return windows.HTRIGHT
}
p := f32.Pt(float32(x), float32(y))
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
return windows.HTCAPTION
}
return windows.HTCLIENT
}
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
if !w.focused {
if !w.config.Focused {
windows.SetFocus(w.hwnd)
}
var typ pointer.Type
var kind pointer.Kind
if press {
typ = pointer.Press
kind = pointer.Press
if w.pointerBtns == 0 {
windows.SetCapture(w.hwnd)
}
w.pointerBtns |= btn
} else {
typ = pointer.Release
kind = pointer.Release
w.pointerBtns &^= btn
if w.pointerBtns == 0 {
windows.ReleaseCapture()
@@ -503,8 +511,8 @@ func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr,
}
x, y := coordsFromlParam(lParam)
p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{
Type: typ,
w.ProcessEvent(pointer.Event{
Kind: kind,
Source: pointer.Mouse,
Position: p,
Buttons: w.pointerBtns,
@@ -538,8 +546,8 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
sp.Y = -dist
}
}
w.w.Event(pointer.Event{
Type: pointer.Scroll,
w.ProcessEvent(pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
Position: p,
Buttons: w.pointerBtns,
@@ -550,7 +558,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
}
// 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)
loop:
for {
@@ -561,7 +569,7 @@ loop:
}
switch ret := windows.GetMessage(msg, 0, 0, 0); ret {
case -1:
return errors.New("GetMessage failed")
panic(errors.New("GetMessage failed"))
case 0:
// WM_QUIT received.
break loop
@@ -569,7 +577,6 @@ loop:
windows.TranslateMessage(msg)
windows.DispatchMessage(msg)
}
return nil
}
func (w *window) EditorStateChanged(old, new editorState) {
@@ -587,16 +594,37 @@ func (w *window) SetAnimating(anim bool) {
w.animating = anim
}
func (w *window) Wakeup() {
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
panic(err)
}
func (w *window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
w.loop.FlushEvents()
}
func (w *window) setStage(s system.Stage) {
if s != w.stage {
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
func (w *window) Event() event.Event {
return w.loop.Event()
}
func (w *window) Invalidate() {
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
}
func (w *window) wakeup() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.hwnd == 0 {
w.loop.Wakeup()
w.loop.FlushEvents()
return
}
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
panic(err)
}
}
@@ -606,8 +634,8 @@ func (w *window) draw(sync bool) {
}
dpi := windows.GetWindowDPI(w.hwnd)
cfg := configForDPI(dpi)
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: cfg,
@@ -653,7 +681,12 @@ func (w *window) readClipboard() error {
}
defer windows.GlobalUnlock(mem)
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
}
@@ -669,9 +702,6 @@ func (w *window) Configure(options []Option) {
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
style &^= winStyle
if !w.config.Decorated {
winStyle = 0
}
switch w.config.Mode {
case Minimized:
style |= winStyle
@@ -684,45 +714,48 @@ func (w *window) Configure(options []Option) {
showMode = windows.SW_SHOWMAXIMIZED
case Windowed:
windows.SetWindowText(w.hwnd, w.config.Title)
style |= winStyle
showMode = windows.SW_SHOWNORMAL
// Get target for client areaa size.
// Get target for client area size.
width = int32(w.config.Size.X)
height = int32(w.config.Size.Y)
// Get the current window size and position.
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
y = wr.Top
width = r.Right - r.Left
height = r.Bottom - r.Top
if w.config.Decorated {
// Compute client size and position. Note that the client size is
// equal to the window size when we are in control of decorations.
r := windows.Rect{
Right: width,
Bottom: height,
}
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
width = r.Right - r.Left
height = r.Bottom - r.Top
}
if !w.config.Decorated {
// Enable drop shadows when we draw decorations.
windows.DwmExtendFrameIntoClientArea(w.hwnd, windows.Margins{-1, -1, -1, -1})
}
case Fullscreen:
swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE
mi := windows.GetMonitorInfo(w.hwnd)
x, y = mi.Monitor.Left, mi.Monitor.Top
width = mi.Monitor.Right - mi.Monitor.Left
height = mi.Monitor.Bottom - mi.Monitor.Top
showMode = windows.SW_SHOW
showMode = windows.SW_SHOWMAXIMIZED
}
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style)
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle)
windows.ShowWindow(w.hwnd, showMode)
w.w.Event(ConfigEvent{Config: w.config})
w.update()
}
func (w *window) WriteClipboard(s string) {
w.writeClipboard(s)
func (w *window) WriteClipboard(mime string, s []byte) {
w.writeClipboard(string(s))
}
func (w *window) writeClipboard(s string) error {
@@ -850,11 +883,11 @@ func (w *window) raise() {
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' {
return string(rune(code)), true
return key.Name(rune(code)), true
}
var r string
var r key.Name
switch code {
case windows.VK_ESCAPE:
@@ -954,4 +987,5 @@ func configForDPI(dpi int) unit.Metric {
}
}
func (_ ViewEvent) ImplementsEvent() {}
func (Win32ViewEvent) implementsViewEvent() {}
func (Win32ViewEvent) ImplementsEvent() {}
+127 -91
View File
@@ -30,16 +30,20 @@ import (
"errors"
"fmt"
"image"
"io"
"strconv"
"strings"
"sync"
"time"
"unsafe"
"gioui.org/f32"
"gioui.org/io/clipboard"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/unit"
syscall "golang.org/x/sys/unix"
@@ -91,12 +95,10 @@ type x11Window struct {
// _NET_WM_STATE_MAXIMIZED_VERT
wmStateMaximizedVert C.Atom
}
stage system.Stage
metric unit.Metric
notify struct {
read, write int
}
dead bool
animating bool
@@ -109,6 +111,11 @@ type x11Window struct {
config Config
wakeups chan struct{}
handler x11EventHandler
buf [100]byte
// invMy avoids the race between destroy and Invalidate.
invMu sync.Mutex
}
var (
@@ -151,8 +158,8 @@ func (w *x11Window) ReadClipboard() {
C.XConvertSelection(w.x, w.atoms.clipboard, w.atoms.utf8string, w.atoms.clipboardContent, w.xw, C.CurrentTime)
}
func (w *x11Window) WriteClipboard(s string) {
w.clipboard.content = []byte(s)
func (w *x11Window) WriteClipboard(mime string, s []byte) {
w.clipboard.content = s
C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime)
C.XSetSelectionOwner(w.x, w.atoms.primary, w.xw, C.CurrentTime)
}
@@ -232,7 +239,7 @@ func (w *x11Window) Configure(options []Option) {
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
}
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func (w *x11Window) setTitle(prev, cnf Config) {
@@ -375,11 +382,47 @@ func (w *x11Window) sendWMStateEvent(action C.long, atom1, atom2 C.ulong) {
var x11OneByte = make([]byte, 1)
func (w *x11Window) Wakeup() {
func (w *x11Window) ProcessEvent(e event.Event) {
w.w.ProcessEvent(e)
}
func (w *x11Window) shutdown(err error) {
w.ProcessEvent(X11ViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
}
func (w *x11Window) Event() event.Event {
for {
evt, ok := w.w.nextEvent()
if !ok {
w.dispatch()
continue
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
}
return evt
}
}
func (w *x11Window) Run(f func()) {
f()
}
func (w *x11Window) Frame(frame *op.Ops) {
w.w.ProcessFrame(frame, nil)
}
func (w *x11Window) Invalidate() {
select {
case w.wakeups <- struct{}{}:
default:
}
w.invMu.Lock()
defer w.invMu.Unlock()
if w.x == nil {
return
}
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
@@ -393,16 +436,20 @@ func (w *x11Window) window() (C.Window, int, int) {
return w.xw, w.config.Size.X, w.config.Size.Y
}
func (w *x11Window) setStage(s system.Stage) {
if s == w.stage {
func (w *x11Window) dispatch() {
if w.x == nil {
// Only Invalidate can wake us up.
<-w.wakeups
w.w.Invalidate()
return
}
w.stage = s
w.w.Event(system.StageEvent{Stage: s})
}
func (w *x11Window) loop() {
h := x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
select {
case <-w.wakeups:
w.w.Invalidate()
default:
}
xfd := C.XConnectionNumber(w.x)
// Poll for events and notifications.
@@ -412,64 +459,52 @@ func (w *x11Window) loop() {
}
xEvents := &pollfds[0].Revents
// Plenty of room for a backlog of notifications.
buf := make([]byte, 100)
loop:
for !w.dead {
var syn, anim bool
// Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll.
if syn = h.handleEvents(); !syn {
anim = w.animating
if !anim {
// Clear poll events.
*xEvents = 0
// Wait for X event or gio notification.
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
}
switch {
case *xEvents&syscall.POLLIN != 0:
syn = h.handleEvents()
if w.dead {
break loop
}
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
break loop
}
var syn, anim bool
// Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll.
if syn = w.handler.handleEvents(); !syn {
anim = w.animating
if !anim {
// Clear poll events.
*xEvents = 0
// Wait for X event or gio notification.
if _, err := syscall.Poll(pollfds, -1); err != nil && err != syscall.EINTR {
panic(fmt.Errorf("x11 loop: poll failed: %w", err))
}
switch {
case *xEvents&syscall.POLLIN != 0:
syn = w.handler.handleEvents()
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
}
}
// Clear notifications.
for {
_, err := syscall.Read(w.notify.read, buf)
if err == syscall.EAGAIN {
break
}
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
}
}
// Clear notifications.
for {
_, err := syscall.Read(w.notify.read, w.buf[:])
if err == syscall.EAGAIN {
break
}
select {
case <-w.wakeups:
w.w.Event(wakeupEvent{})
default:
}
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.w.Event(frameEvent{
FrameEvent: system.FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: w.metric,
},
Sync: syn,
})
if err != nil {
panic(fmt.Errorf("x11 loop: read from notify pipe failed: %w", err))
}
}
if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 {
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: w.config.Size,
Metric: w.metric,
},
Sync: syn,
})
}
}
func (w *x11Window) destroy() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.notify.write != 0 {
syscall.Close(w.notify.write)
w.notify.write = 0
@@ -484,6 +519,7 @@ func (w *x11Window) destroy() {
}
C.XDestroyWindow(w.x, w.xw)
C.XCloseDisplay(w.x)
w.x = nil
}
// atom is a wrapper around XInternAtom. Callers should cache the result
@@ -541,13 +577,13 @@ func (h *x11EventHandler) handleEvents() bool {
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.w.Event(e)
w.ProcessEvent(e)
}
}
case C.ButtonPress, C.ButtonRelease:
bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
ev := pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Source: pointer.Mouse,
Position: f32.Point{
X: float32(bevt.x),
@@ -557,7 +593,7 @@ func (h *x11EventHandler) handleEvents() bool {
Modifiers: w.xkb.Modifiers(),
}
if bevt._type == C.ButtonRelease {
ev.Type = pointer.Release
ev.Kind = pointer.Release
}
var btn pointer.Buttons
const scrollScale = 10
@@ -569,7 +605,7 @@ func (h *x11EventHandler) handleEvents() bool {
case C.Button3:
btn = pointer.ButtonSecondary
case C.Button4:
ev.Type = pointer.Scroll
ev.Kind = pointer.Scroll
// scroll up or left (if shift is pressed).
if ev.Modifiers == key.ModShift {
ev.Scroll.X = -scrollScale
@@ -578,7 +614,7 @@ func (h *x11EventHandler) handleEvents() bool {
}
case C.Button5:
// scroll down or right (if shift is pressed).
ev.Type = pointer.Scroll
ev.Kind = pointer.Scroll
if ev.Modifiers == key.ModShift {
ev.Scroll.X = +scrollScale
} else {
@@ -587,11 +623,11 @@ func (h *x11EventHandler) handleEvents() bool {
case 6:
// http://xahlee.info/linux/linux_x11_mouse_button_number.html
// scroll left.
ev.Type = pointer.Scroll
ev.Kind = pointer.Scroll
ev.Scroll.X = -scrollScale * 2
case 7:
// scroll right
ev.Type = pointer.Scroll
ev.Kind = pointer.Scroll
ev.Scroll.X = +scrollScale * 2
default:
continue
@@ -603,11 +639,11 @@ func (h *x11EventHandler) handleEvents() bool {
w.pointerBtns &^= btn
}
ev.Buttons = w.pointerBtns
w.w.Event(ev)
w.ProcessEvent(ev)
case C.MotionNotify:
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
w.w.Event(pointer.Event{
Type: pointer.Move,
w.ProcessEvent(pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: f32.Point{
@@ -621,14 +657,16 @@ func (h *x11EventHandler) handleEvents() bool {
// redraw only on the last expose event
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
case C.FocusIn:
w.w.Event(key.FocusEvent{Focus: true})
w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
case C.FocusOut:
w.w.Event(key.FocusEvent{Focus: false})
w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
case C.ConfigureNotify: // window configuration change
cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
w.config.Size = sz
w.w.Event(ConfigEvent{Config: w.config})
w.ProcessEvent(ConfigEvent{Config: w.config})
}
// redraw will be done by a later expose event
case C.SelectionNotify:
@@ -650,7 +688,12 @@ func (h *x11EventHandler) handleEvents() bool {
break
}
str := C.GoStringN((*C.char)(unsafe.Pointer(text.value)), C.int(text.nitems))
w.w.Event(clipboard.Event{Text: str})
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(str))
},
})
case C.SelectionRequest:
cevt := (*C.XSelectionRequestEvent)(unsafe.Pointer(xev))
if (cevt.selection != w.atoms.clipboard && cevt.selection != w.atoms.primary) || cevt.property == C.None {
@@ -704,7 +747,7 @@ func (h *x11EventHandler) handleEvents() bool {
cevt := (*C.XClientMessageEvent)(unsafe.Pointer(xev))
switch *(*C.long)(unsafe.Pointer(&cevt.data)) {
case C.long(w.atoms.evDelWindow):
w.dead = true
w.shutdown(nil)
return false
}
}
@@ -786,8 +829,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
wakeups: make(chan struct{}, 1),
config: Config{Size: cnf.Size},
}
w.handler = x11EventHandler{w: w, xev: new(C.XEvent), text: make([]byte, 4)}
w.notify.read = pipe[0]
w.notify.write = pipe[1]
w.w.SetDriver(w)
if err := w.updateXkbKeymap(); err != nil {
w.destroy()
@@ -823,19 +868,10 @@ func newX11Window(gioWin *callbacks, options []Option) error {
// extensions
C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1)
go func() {
w.w.SetDriver(w)
// make the window visible on the screen
C.XMapWindow(dpy, win)
w.Configure(options)
w.w.Event(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
w.setStage(system.StageRunning)
w.loop()
w.w.Event(X11ViewEvent{})
w.w.Event(system.DestroyEvent{Err: nil})
w.destroy()
}()
// make the window visible on the screen
C.XMapWindow(dpy, win)
w.Configure(options)
w.ProcessEvent(X11ViewEvent{Display: unsafe.Pointer(dpy), Window: uintptr(win)})
return nil
}
+1 -1
View File
@@ -25,6 +25,6 @@ func runMain() {
// Indirect call, since the linker does not know the address of main when
// laying down this package.
fn := mainMain
fn()
go fn()
})
}
+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: Unlicense OR MIT
package app
// DestroyEvent is the last event sent through
// a window event channel.
type DestroyEvent struct {
// Err is nil for normal window closures. If a
// window is prematurely closed, Err is the cause.
Err error
}
func (DestroyEvent) ImplementsEvent() {}
+366 -598
View File
File diff suppressed because it is too large Load Diff
Generated
+48 -17
View File
@@ -9,11 +9,11 @@
]
},
"locked": {
"lastModified": 1659298920,
"narHash": "sha256-LgRMge8BZUG15EN43iDJOlnEMX1dvRprB7SaoNqgibU=",
"lastModified": 1701721028,
"narHash": "sha256-2z4YrdHPLoMZNWR1MPOjNZMqPg057i1eZXaYI6RTahQ=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "d4f20a3cd4ce961bb23b48447457f6810d69ae5e",
"rev": "c923f9ec0f4dd0d7dc725dc5b73fbf03658e50dd",
"type": "github"
},
"original": {
@@ -24,21 +24,18 @@
},
"devshell": {
"inputs": {
"flake-utils": [
"android",
"nixpkgs"
],
"nixpkgs": [
"android",
"nixpkgs"
]
],
"systems": "systems"
},
"locked": {
"lastModified": 1658746384,
"narHash": "sha256-CCJcoMOcXyZFrV1ag4XMTpAPjLWb4Anbv+ktXFI1ry0=",
"lastModified": 1701697687,
"narHash": "sha256-dLLE5wQBVv+pIb4bWmKFSw2DvLVyuEk0F7ng6hpZPSU=",
"owner": "numtide",
"repo": "devshell",
"rev": "0ffc7937bb5e8141af03d462b468bd071eb18e1b",
"rev": "c3bd77911391eb1638af6ce773de86da57ee6df5",
"type": "github"
},
"original": {
@@ -48,12 +45,15 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -64,15 +64,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1659305579,
"narHash": "sha256-SFeQTmh7hc9Y2fSkooHaoS8mDfPa04sfmUCtQ8MA6Pg=",
"lastModified": 1701282334,
"narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5857574d45925585baffde730369414319228a84",
"rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "23.11",
"repo": "nixpkgs",
"type": "github"
}
@@ -82,6 +83,36 @@
"android": "android",
"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",
+4 -4
View File
@@ -3,7 +3,7 @@
description = "Gio build environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
nixpkgs.url = "github:NixOS/nixpkgs/23.11";
android.url = "github:tadfisher/android-nixpkgs";
android.inputs.nixpkgs.follows = "nixpkgs";
};
@@ -33,10 +33,10 @@
default = with pkgs; mkShell
({
ANDROID_SDK_ROOT = "${android-sdk}/share/android-sdk";
JAVA_HOME = jdk8.home;
JAVA_HOME = jdk17.home;
packages = [
android-sdk
jdk8
jdk17
clang
] ++ (if stdenv.isLinux then [
vulkan-headers
@@ -46,7 +46,7 @@
xorg.libXcursor
xorg.libXfixes
libGL
pkgconfig
pkg-config
] else if stdenv.isDarwin then [
darwin.apple_sdk_11_0.frameworks.Foundation
darwin.apple_sdk_11_0.frameworks.Metal
+95 -86
View File
@@ -18,6 +18,7 @@ import (
"gioui.org/f32"
"gioui.org/internal/fling"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/op"
@@ -37,20 +38,24 @@ type Hover struct {
// Add the gesture to detect hovering over the current pointer area.
func (h *Hover) Add(ops *op.Ops) {
pointer.InputOp{
Tag: h,
Types: pointer.Enter | pointer.Leave,
}.Add(ops)
event.Op(ops, h)
}
// Hovered returns whether a pointer is inside the area.
func (h *Hover) Hovered(q event.Queue) bool {
for _, ev := range q.Events(h) {
// Update state and report whether a pointer is inside the area.
func (h *Hover) Update(q input.Source) bool {
for {
ev, ok := q.Event(pointer.Filter{
Target: h,
Kinds: pointer.Enter | pointer.Leave | pointer.Cancel,
})
if !ok {
break
}
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
switch e.Kind {
case pointer.Leave, pointer.Cancel:
if h.entered && h.pid == e.PointerID {
h.entered = false
@@ -87,10 +92,10 @@ type Click struct {
}
// ClickEvent represent a click action, either a
// TypePress for the beginning of a click or a
// TypeClick for a completed click.
// KindPress for the beginning of a click or a
// KindClick for a completed click.
type ClickEvent struct {
Type ClickType
Kind ClickKind
Position image.Point
Source pointer.Source
Modifiers key.Modifiers
@@ -99,7 +104,7 @@ type ClickEvent struct {
NumClicks int
}
type ClickType uint8
type ClickKind uint8
// Drag detects drag gestures in the form of pointer.Drag events.
type Drag struct {
@@ -107,7 +112,6 @@ type Drag struct {
pressed bool
pid pointer.ID
start f32.Point
grab bool
}
// Scroll detects scroll gestures and reduces them to
@@ -115,11 +119,9 @@ type Drag struct {
// movements as well as drag and fling touch gestures.
type Scroll struct {
dragging bool
axis Axis
estimator fling.Extrapolation
flinger fling.Animation
pid pointer.ID
grab bool
last int
// Leftover scroll.
scroll float32
@@ -136,15 +138,15 @@ const (
)
const (
// TypePress is reported for the first pointer
// KindPress is reported for the first pointer
// press.
TypePress ClickType = iota
// TypeClick is reported when a click action
KindPress ClickKind = iota
// KindClick is reported when a click action
// is complete.
TypeClick
// TypeCancel is reported when the gesture is
KindClick
// KindCancel is reported when the gesture is
// cancelled.
TypeCancel
KindCancel
)
const (
@@ -161,10 +163,7 @@ const touchSlop = unit.Dp(3)
// Add the handler to the operation list to receive click events.
func (c *Click) Add(ops *op.Ops) {
pointer.InputOp{
Tag: c,
Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
}.Add(ops)
event.Op(ops, c)
}
// Hovered returns whether a pointer is inside the area.
@@ -177,24 +176,36 @@ func (c *Click) Pressed() bool {
return c.pressed
}
// Events returns the next click events, if any.
func (c *Click) Events(q event.Queue) []ClickEvent {
var events []ClickEvent
for _, evt := range q.Events(c) {
// Update state and return the next click events, if any.
func (c *Click) Update(q input.Source) (ClickEvent, bool) {
for {
evt, ok := q.Event(pointer.Filter{
Target: c,
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave | pointer.Cancel,
})
if !ok {
break
}
e, ok := evt.(pointer.Event)
if !ok {
continue
}
switch e.Type {
switch e.Kind {
case pointer.Release:
if !c.pressed || c.pid != e.PointerID {
break
}
c.pressed = false
if !c.entered || c.hovered {
events = append(events, ClickEvent{Type: TypeClick, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
return ClickEvent{
Kind: KindClick,
Position: e.Position.Round(),
Source: e.Source,
Modifiers: e.Modifiers,
NumClicks: c.clicks,
}, true
} else {
events = append(events, ClickEvent{Type: TypeCancel})
return ClickEvent{Kind: KindCancel}, true
}
case pointer.Cancel:
wasPressed := c.pressed
@@ -202,7 +213,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
c.hovered = false
c.entered = false
if wasPressed {
events = append(events, ClickEvent{Type: TypeCancel})
return ClickEvent{Kind: KindCancel}, true
}
case pointer.Press:
if c.pressed {
@@ -224,7 +235,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
c.clicks = 1
}
c.clickedAt = e.Time
events = append(events, ClickEvent{Type: TypePress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
return ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks}, true
case pointer.Leave:
if !c.pressed {
c.pid = e.PointerID
@@ -242,25 +253,16 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
}
}
}
return events
return ClickEvent{}, false
}
func (ClickEvent) ImplementsEvent() {}
// Add the handler to the operation list to receive scroll events.
// The bounds variable refers to the scrolling boundaries
// as defined in io/pointer.InputOp.
func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
oph := pointer.InputOp{
Tag: s,
Grab: s.grab,
Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
ScrollBounds: bounds,
}
oph.Add(ops)
if s.flinger.Active() {
op.InvalidateOp{}.Add(ops)
}
// as defined in [pointer.Filter].
func (s *Scroll) Add(ops *op.Ops) {
event.Op(ops, s)
}
// Stop any remaining fling movement.
@@ -268,20 +270,24 @@ func (s *Scroll) Stop() {
s.flinger = fling.Animation{}
}
// Scroll detects the scrolling distance from the available events and
// ongoing fling gestures.
func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
if s.axis != axis {
s.axis = axis
return 0
}
// Update state and report the scroll distance along axis.
func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, bounds image.Rectangle) int {
total := 0
for _, evt := range q.Events(s) {
f := pointer.Filter{
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollBounds: bounds,
}
for {
evt, ok := q.Event(f)
if !ok {
break
}
e, ok := evt.(pointer.Event)
if !ok {
continue
}
switch e.Type {
switch e.Kind {
case pointer.Press:
if s.dragging {
break
@@ -293,7 +299,7 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
}
s.Stop()
s.estimator = fling.Extrapolation{}
v := s.val(e.Position)
v := s.val(axis, e.Position)
s.last = int(math.Round(float64(v)))
s.estimator.Sample(e.Time, v)
s.dragging = true
@@ -309,9 +315,8 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
fallthrough
case pointer.Cancel:
s.dragging = false
s.grab = false
case pointer.Scroll:
switch s.axis {
switch axis {
case Horizontal:
s.scroll += e.Scroll.X
case Vertical:
@@ -324,14 +329,14 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
if !s.dragging || s.pid != e.PointerID {
continue
}
val := s.val(e.Position)
val := s.val(axis, e.Position)
s.estimator.Sample(e.Time, val)
v := int(math.Round(float64(val)))
dist := s.last - v
if e.Priority < pointer.Grabbed {
slop := cfg.Dp(touchSlop)
if dist := dist; dist >= slop || -slop >= dist {
s.grab = true
q.Execute(pointer.GrabCmd{Tag: s, ID: e.PointerID})
}
} else {
s.last = v
@@ -340,11 +345,14 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
}
}
total += s.flinger.Tick(t)
if s.flinger.Active() {
q.Execute(op.InvalidateCmd{})
}
return total
}
func (s *Scroll) val(p f32.Point) float32 {
if s.axis == Horizontal {
func (s *Scroll) val(axis Axis, p f32.Point) float32 {
if axis == Horizontal {
return p.X
} else {
return p.Y
@@ -365,23 +373,25 @@ func (s *Scroll) State() ScrollState {
// Add the handler to the operation list to receive drag events.
func (d *Drag) Add(ops *op.Ops) {
pointer.InputOp{
Tag: d,
Grab: d.grab,
Types: pointer.Press | pointer.Drag | pointer.Release,
}.Add(ops)
event.Op(ops, d)
}
// Events returns the next drag events, if any.
func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
var events []pointer.Event
for _, e := range q.Events(d) {
e, ok := e.(pointer.Event)
// Update state and return the next drag event, if any.
func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) (pointer.Event, bool) {
for {
ev, ok := q.Event(pointer.Filter{
Target: d,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel,
})
if !ok {
break
}
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Type {
switch e.Kind {
case pointer.Press:
if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
continue
@@ -409,7 +419,7 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
diff := e.Position.Sub(d.start)
slop := cfg.Dp(touchSlop)
if diff.X*diff.X+diff.Y*diff.Y > float32(slop*slop) {
d.grab = true
q.Execute(pointer.GrabCmd{Tag: d, ID: e.PointerID})
}
}
case pointer.Release, pointer.Cancel:
@@ -418,13 +428,12 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
continue
}
d.dragging = false
d.grab = false
}
events = append(events, e)
return e, true
}
return events
return pointer.Event{}, false
}
// Dragging reports whether it is currently in use.
@@ -444,16 +453,16 @@ func (a Axis) String() string {
}
}
func (ct ClickType) String() string {
func (ct ClickKind) String() string {
switch ct {
case TypePress:
return "TypePress"
case TypeClick:
return "TypeClick"
case TypeCancel:
return "TypeCancel"
case KindPress:
return "KindPress"
case KindClick:
return "KindClick"
case KindCancel:
return "KindCancel"
default:
panic("invalid ClickType")
panic("invalid ClickKind")
}
}
+21 -21
View File
@@ -9,8 +9,8 @@ import (
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/op"
"gioui.org/op/clip"
)
@@ -22,20 +22,21 @@ func TestHover(t *testing.T) {
stack := clip.Rect(rect).Push(ops)
h.Add(ops)
stack.Pop()
r := new(router.Router)
r := new(input.Router)
h.Update(r.Source())
r.Frame(ops)
r.Queue(
pointer.Event{Type: pointer.Move, Position: f32.Pt(30, 30)},
pointer.Event{Kind: pointer.Move, Position: f32.Pt(30, 30)},
)
if !h.Hovered(r) {
if !h.Update(r.Source()) {
t.Fatal("expected hovered")
}
r.Queue(
pointer.Event{Type: pointer.Move, Position: f32.Pt(50, 50)},
pointer.Event{Kind: pointer.Move, Position: f32.Pt(50, 50)},
)
if h.Hovered(r) {
if h.Update(r.Source()) {
t.Fatal("expected not hovered")
}
}
@@ -71,12 +72,21 @@ func TestMouseClicks(t *testing.T) {
var ops op.Ops
click.Add(&ops)
var r router.Router
var r input.Router
click.Update(r.Source())
r.Frame(&ops)
r.Queue(tc.events...)
events := click.Events(&r)
clicks := filterMouseClicks(events)
var clicks []ClickEvent
for {
ev, ok := click.Update(r.Source())
if !ok {
break
}
if ev.Kind == KindClick {
clicks = append(clicks, ev)
}
}
if got, want := len(clicks), len(tc.clicks); got != want {
t.Fatalf("got %d mouse clicks, expected %d", got, want)
}
@@ -92,7 +102,7 @@ func TestMouseClicks(t *testing.T) {
func mouseClickEvents(times ...time.Duration) []event.Event {
press := pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
}
@@ -101,18 +111,8 @@ func mouseClickEvents(times ...time.Duration) []event.Event {
press := press
press.Time = t
release := press
release.Type = pointer.Release
release.Kind = pointer.Release
events = append(events, press, release)
}
return events
}
func filterMouseClicks(events []ClickEvent) []ClickEvent {
var clicks []ClickEvent
for _, ev := range events {
if ev.Type == TypeClick {
clicks = append(clicks, ev)
}
}
return clicks
}
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.19
require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
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
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
+2 -2
View File
@@ -3,8 +3,8 @@ 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-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
+15 -10
View File
@@ -8,8 +8,13 @@ import (
"gioui.org/internal/f32"
)
type resourceCache struct {
res map[interface{}]resourceCacheValue
type textureCacheKey struct {
filter byte
handle any
}
type textureCache struct {
res map[textureCacheKey]resourceCacheValue
}
type resourceCacheValue struct {
@@ -37,13 +42,13 @@ type opCacheValue struct {
keep bool
}
func newResourceCache() *resourceCache {
return &resourceCache{
res: make(map[interface{}]resourceCacheValue),
func newTextureCache() *textureCache {
return &textureCache{
res: make(map[textureCacheKey]resourceCacheValue),
}
}
func (r *resourceCache) get(key interface{}) (resource, bool) {
func (r *textureCache) get(key textureCacheKey) (resource, bool) {
v, exists := r.res[key]
if !exists {
return nil, false
@@ -55,17 +60,17 @@ func (r *resourceCache) get(key interface{}) (resource, bool) {
return v.resource, exists
}
func (r *resourceCache) put(key interface{}, val resource) {
func (r *textureCache) put(key textureCacheKey, val resource) {
v, exists := r.res[key]
if exists && v.used {
panic(fmt.Errorf("key exists, %p", key))
panic(fmt.Errorf("key exists, %v", key))
}
v.used = true
v.resource = val
r.res[key] = v
}
func (r *resourceCache) frame() {
func (r *textureCache) frame() {
for k, v := range r.res {
if v.used {
v.used = false
@@ -77,7 +82,7 @@ func (r *resourceCache) frame() {
}
}
func (r *resourceCache) release() {
func (r *textureCache) release() {
for _, v := range r.res {
v.resource.release()
}
-24
View File
@@ -1,24 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
package gpu
import "testing"
func BenchmarkResourceCache(b *testing.B) {
offset := 0
const N = 100
cache := newResourceCache()
for i := 0; i < b.N; i++ {
// half are the same and half updated
for k := 0; k < N; k++ {
cache.put(offset+k, nullResource{})
}
cache.frame()
offset += N / 2
}
}
type nullResource struct{}
func (nullResource) release() {}
+3 -12
View File
@@ -93,7 +93,6 @@ type compute struct {
}
}
timers struct {
profile string
t *timers
compact *timer
render *timer
@@ -176,7 +175,6 @@ type materialUniforms struct {
type collector struct {
hasher maphash.Hash
profile bool
reader ops.Reader
states []f32.Affine2D
clear bool
@@ -597,7 +595,7 @@ func (g *compute) frame(target RenderTarget) error {
defer g.ctx.EndFrame()
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.compact = t.t.newTimer()
t.render = t.t.newTimer()
@@ -631,13 +629,13 @@ func (g *compute) frame(target RenderTarget) error {
return err
}
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
ft := com + ren + blit
q := 100 * time.Microsecond
ft = ft.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
}
@@ -661,10 +659,6 @@ func (g *compute) dumpAtlases() {
}
}
func (g *compute) Profile() string {
return g.timers.profile
}
func (g *compute) compactAllocs() error {
const (
maxAllocAge = 3
@@ -1656,7 +1650,6 @@ func (e *encoder) line(start, end f32.Point) {
func (c *collector) reset() {
c.prevFrame, c.frame = c.frame, c.prevFrame
c.profile = false
c.clipStates = c.clipStates[:0]
c.transStack = c.transStack[:0]
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)
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeProfile:
c.profile = true
case ops.TypeTransform:
dop, push := ops.DecodeTransform(encOp.Data)
if push {
+229 -61
View File
@@ -44,14 +44,10 @@ type GPU interface {
Clear(color color.NRGBA)
// Frame draws the graphics operations from op into a viewport of target.
Frame(frame *op.Ops, target RenderTarget, viewport image.Point) error
// Profile returns the last available profiling information. Profiling
// information is requested when Frame sees an io/profile.Op, and the result
// is available through Profile at some later time.
Profile() string
}
type gpu struct {
cache *resourceCache
cache *textureCache
profile string
timers *timers
@@ -68,22 +64,39 @@ type renderer struct {
pather *pather
packer packer
intersections packer
layers packer
layerFBOs fboSet
}
type drawOps struct {
profile bool
reader ops.Reader
states []f32.Affine2D
transStack []f32.Affine2D
vertCache []byte
viewport image.Point
clear bool
clearColor f32color.RGBA
imageOps []imageOp
pathOps []*pathOp
pathOpCache []pathOp
qs quadSplitter
pathCache *opCache
reader ops.Reader
states []f32.Affine2D
transStack []f32.Affine2D
layers []opacityLayer
opacityStack []int
vertCache []byte
viewport image.Point
clear bool
clearColor f32color.RGBA
imageOps []imageOp
pathOps []*pathOp
pathOpCache []pathOp
qs quadSplitter
pathCache *opCache
}
type opacityLayer struct {
opacity float32
parent int
// depth of the opacity stack. Layers of equal depth are
// independent and may be packed into one atlas.
depth int
// opStart and opEnd denote the range of drawOps.imageOps
// that belong to the layer.
opStart, opEnd int
// clip of the layer operations.
clip image.Rectangle
place placement
}
type drawState struct {
@@ -127,7 +140,12 @@ type imageOp struct {
clip image.Rectangle
material material
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 {
@@ -154,17 +172,25 @@ type material struct {
// For materialTypeColor.
color f32color.RGBA
// For materialTypeLinearGradient.
color1 f32color.RGBA
color2 f32color.RGBA
color1 f32color.RGBA
color2 f32color.RGBA
opacity float32
// For materialTypeTexture.
data imageOpData
tex driver.Texture
uvTrans f32.Affine2D
}
const (
filterLinear = 0
filterNearest = 1
)
// imageOpData is the shadow of paint.ImageOp.
type imageOpData struct {
src *image.RGBA
handle interface{}
filter byte
}
type linearGradientOpData struct {
@@ -182,6 +208,7 @@ func decodeImageOp(data []byte, refs []interface{}) imageOpData {
return imageOpData{
src: refs[0].(*image.RGBA),
handle: handle,
filter: data[1],
}
}
@@ -222,8 +249,6 @@ func decodeLinearGradientOp(data []byte) linearGradientOpData {
}
}
type clipType uint8
type resource interface {
release()
}
@@ -273,6 +298,9 @@ type blitUniforms struct {
transform [4]float32
uvTransformR1 [4]float32
uvTransformR2 [4]float32
opacity float32
fbo float32
_ [2]float32
}
type colorUniforms struct {
@@ -284,7 +312,7 @@ type gradientUniforms struct {
color2 f32color.RGBA
}
type materialType uint8
type clipType uint8
const (
clipTypeNone clipType = iota
@@ -292,6 +320,8 @@ const (
clipTypeIntersection
)
type materialType uint8
const (
materialColor materialType = iota
materialLinearGradient
@@ -324,7 +354,7 @@ func NewWithDevice(d driver.Device) (GPU, error) {
func newGPU(ctx driver.Device) (*gpu, error) {
g := &gpu{
cache: newResourceCache(),
cache: newTextureCache(),
}
g.drawOps.pathCache = newOpCache()
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.drawOps.reset(viewport)
g.drawOps.collect(frameOps, viewport)
if g.drawOps.profile && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
if false && g.timers == nil && g.ctx.Caps().Features.Has(driver.FeatureTimers) {
g.frameStart = time.Now()
g.timers = newTimers(g.ctx)
g.stencilTimer = g.timers.newTimer()
@@ -390,7 +420,9 @@ func (g *gpu) frame(target RenderTarget) error {
g.stencilTimer.end()
g.coverTimer.begin()
g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
g.renderer.prepareDrawOps(g.drawOps.imageOps)
g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
g.renderer.drawLayers(g.drawOps.layers, g.drawOps.imageOps)
d := driver.LoadDesc{
ClearColor: g.drawOps.clearColor,
}
@@ -400,14 +432,14 @@ func (g *gpu) frame(target RenderTarget) error {
}
g.ctx.BeginRenderPass(defFBO, d)
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
g.renderer.drawOps(g.cache, g.drawOps.imageOps)
g.renderer.drawOps(false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
g.coverTimer.end()
g.ctx.EndRenderPass()
g.cleanupTimer.begin()
g.cache.frame()
g.drawOps.pathCache.frame()
g.cleanupTimer.end()
if g.drawOps.profile && g.timers.ready() {
if false && g.timers.ready() {
st, covt, cleant := g.stencilTimer.Elapsed, g.coverTimer.Elapsed, g.cleanupTimer.Elapsed
ft := st + covt + cleant
q := 100 * time.Microsecond
@@ -423,20 +455,38 @@ func (g *gpu) Profile() string {
return g.profile
}
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
func (r *renderer) texHandle(cache *textureCache, data imageOpData) driver.Texture {
key := textureCacheKey{
filter: data.filter,
handle: data.handle,
}
var tex *texture
t, exists := cache.get(data.handle)
t, exists := cache.get(key)
if !exists {
t = &texture{
src: data.src,
}
cache.put(data.handle, t)
cache.put(key, t)
}
tex = t.(*texture)
if tex.tex != nil {
return tex.tex
}
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA, data.src.Bounds().Dx(), data.src.Bounds().Dy(), driver.FilterLinearMipmapLinear, driver.FilterLinear, driver.BufferBindingTexture)
var minFilter, magFilter driver.TextureFilter
switch data.filter {
case filterLinear:
minFilter, magFilter = driver.FilterLinearMipmapLinear, driver.FilterLinear
case filterNearest:
minFilter, magFilter = driver.FilterNearest, driver.FilterNearest
}
handle, err := r.ctx.NewTexture(driver.TextureFormatSRGBA,
data.src.Bounds().Dx(), data.src.Bounds().Dy(),
minFilter, magFilter,
driver.BufferBindingTexture,
)
if err != nil {
panic(err)
}
@@ -464,15 +514,18 @@ func newRenderer(ctx driver.Device) *renderer {
if cap := 8192; maxDim > cap {
maxDim = cap
}
d := image.Pt(maxDim, maxDim)
r.packer.maxDims = image.Pt(maxDim, maxDim)
r.intersections.maxDims = image.Pt(maxDim, maxDim)
r.packer.maxDims = d
r.intersections.maxDims = d
r.layers.maxDims = d
return r
}
func (r *renderer) release() {
r.pather.release()
r.blitter.release()
r.layerFBOs.delete(r.ctx, 0)
}
func newBlitter(ctx driver.Device) *blitter {
@@ -747,8 +800,7 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
ops = ops[:len(ops)-1]
continue
}
sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()}
place, ok := r.packer.add(sz)
place, ok := r.packer.add(p.clip.Size())
if !ok {
// The clip area is at most the entire screen. Hopefully no
// screen is larger than GL_MAX_TEXTURE_SIZE.
@@ -760,14 +812,92 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
*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.Max.X, v.Max.Y)
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) {
d.profile = false
d.viewport = viewport
d.imageOps = d.imageOps[:0]
d.pathOps = d.pathOps[:0]
d.pathOpCache = d.pathOpCache[:0]
d.vertCache = d.vertCache[:0]
d.transStack = d.transStack[:0]
d.layers = d.layers[:0]
d.opacityStack = d.opacityStack[:0]
}
func (d *drawOps) collect(root *op.Ops, viewport image.Point) {
@@ -853,8 +983,6 @@ func (d *drawOps) collectOps(r *ops.Reader, viewport f32.Rectangle) {
loop:
for encOp, ok := r.Decode(); ok; encOp, ok = r.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeProfile:
d.profile = true
case ops.TypeTransform:
dop, push := ops.DecodeTransform(encOp.Data)
if push {
@@ -866,6 +994,27 @@ loop:
state.t = d.transStack[n-1]
d.transStack = d.transStack[:n-1]
case ops.TypePushOpacity:
opacity := ops.DecodeOpacity(encOp.Data)
parent := -1
depth := len(d.opacityStack)
if depth > 0 {
parent = d.opacityStack[depth-1]
}
lidx := len(d.layers)
d.layers = append(d.layers, opacityLayer{
opacity: opacity,
parent: parent,
depth: depth,
opStart: len(d.imageOps),
})
d.opacityStack = append(d.opacityStack, lidx)
case ops.TypePopOpacity:
n := len(d.opacityStack)
idx := d.opacityStack[n-1]
d.layers[idx].opEnd = len(d.imageOps)
d.opacityStack = d.opacityStack[:n-1]
case ops.TypeStroke:
quads.key.strokeWidth = decodeStrokeOp(encOp.Data)
@@ -958,7 +1107,7 @@ loop:
mat := state.materialFor(bnd, off, partialTrans, bounds)
rect := state.cpath == nil || state.cpath.rect
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) {
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) && len(d.opacityStack) == 0 {
// The image is a uniform opaque color and takes up the whole screen.
// Scrap images up to and including this image and set clear color.
d.imageOps = d.imageOps[:0]
@@ -971,6 +1120,15 @@ loop:
clip: bounds,
material: mat,
}
if n := len(d.opacityStack); n > 0 {
idx := d.opacityStack[n-1]
lb := d.layers[idx].clip
if lb.Empty() {
d.layers[idx].clip = img.clip
} else {
d.layers[idx].clip = lb.Union(img.clip)
}
}
d.imageOps = append(d.imageOps, img)
if clipData != nil {
@@ -1000,7 +1158,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 {
var m material
m := material{
opacity: 1.,
}
switch d.matType {
case materialColor:
m.material = materialColor
@@ -1039,24 +1199,25 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
return m
}
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
for _, img := range ops {
func (r *renderer) uploadImages(cache *textureCache, ops []imageOp) {
for i := range ops {
img := &ops[i]
m := img.material
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 {
m := img.material
switch m.material {
case materialTexture:
r.ctx.PrepareTexture(r.texHandle(cache, m.data))
r.ctx.PrepareTexture(m.tex)
}
var fbo stencilFBO
var fbo FBO
switch img.clipType {
case clipTypeNone:
continue
@@ -1069,24 +1230,26 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
}
}
func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point, ops []imageOp) {
var coverTex driver.Texture
for _, img := range ops {
for i := 0; i < len(ops); i++ {
img := ops[i]
i += img.layerOps
m := img.material
switch m.material {
case materialTexture:
r.ctx.BindTexture(0, 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)
var fbo stencilFBO
scale, off := clipSpaceTransform(drc, viewport)
var fbo FBO
switch img.clipType {
case clipTypeNone:
p := r.blitter.pipelines[m.material]
r.ctx.BindPipeline(p.pipeline)
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
case clipTypePath:
fbo = r.pather.stenciler.cover(img.place.Idx)
@@ -1105,11 +1268,11 @@ func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
p := r.pather.coverer.pipelines[m.material]
r.ctx.BindPipeline(p.pipeline)
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]
b.ctx.BindPipeline(p.pipeline)
var uniforms *blitUniforms
@@ -1119,18 +1282,23 @@ func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.
uniforms = &b.colUniforms.blitUniforms
case materialTexture:
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.uvTransformR1 = [4]float32{t1, t2, t3, 0}
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
case materialLinearGradient:
b.linearGradientUniforms.color1 = col1
b.linearGradientUniforms.color2 = col2
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
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.uvTransformR1 = [4]float32{t1, t2, t3, 0}
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
}
uniforms.fbo = 0
if fbo {
uniforms.fbo = 1
}
uniforms.opacity = opacity
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
p.UploadUniforms(b.ctx)
b.ctx.DrawArrays(0, 4)
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: 334 B

+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) {
ops := new(op.Ops)
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 exercize the glClear optimization.
paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op())
opc2 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op())
opc2.Pop()
opc1.Pop()
opc3 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(50+20, 10), Max: image.Pt(50+64, 128)}.Op())
opc3.Pop()
}, func(r result) {
})
}
// lerp calculates linear interpolation with color b and p.
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
return f32color.RGBA{
+16 -12
View File
@@ -58,7 +58,7 @@ type coverUniforms struct {
uvCoverTransform [4]float32
uvTransformR1 [4]float32
uvTransformR2 [4]float32
_ float32
fbo float32
}
type stenciler struct {
@@ -90,10 +90,10 @@ type intersectUniforms struct {
}
type fboSet struct {
fbos []stencilFBO
fbos []FBO
}
type stencilFBO struct {
type FBO struct {
size image.Point
tex driver.Texture
}
@@ -247,10 +247,10 @@ func newStenciler(ctx driver.Device) *stenciler {
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.
for i := len(s.fbos); i < len(sizes); i++ {
s.fbos = append(s.fbos, stencilFBO{})
s.fbos = append(s.fbos, FBO{})
}
// Resize fbos.
for i, sz := range sizes {
@@ -273,7 +273,7 @@ func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
if 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)
if err != nil {
panic(err)
@@ -340,15 +340,15 @@ func (s *stenciler) beginIntersect(sizes []image.Point) {
// 8 bit coverage is enough, but OpenGL ES only supports single channel
// floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
// no floating point support is available.
s.intersections.resize(s.ctx, 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]
}
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) {
@@ -375,11 +375,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) {
p.coverer.cover(mat, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
func (p *pather) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
p.coverer.cover(mat, isFBO, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
}
func (c *coverer) cover(mat materialType, 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
switch mat {
case materialColor:
@@ -399,6 +399,10 @@ func (c *coverer) cover(mat materialType, col f32color.RGBA, col1, col2 f32color
c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
uniforms = &c.texUniforms.coverUniforms
}
uniforms.fbo = 0
if isFBO {
uniforms.fbo = 1
}
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
c.pipelines[mat].UploadUniforms(c.ctx)
+1
View File
@@ -62,6 +62,7 @@ func (c *Context) Release() {
eglDestroyContext(c.disp, c.eglCtx.ctx)
c.eglCtx = nil
}
eglTerminate(c.disp)
c.disp = nilEGLDisplay
}
+1 -1
View File
@@ -665,7 +665,7 @@ func (f *Functions) load(forceES bool) error {
case runtime.GOOS == "android":
libNames = []string{"libGLESv2.so", "libGLESv3.so"}
default:
libNames = []string{"libGLESv2.so.2"}
libNames = []string{"libGLESv2.so.2", "libGLESv2.so.3.0"}
}
for _, lib := range libNames {
if h := dlopen(lib); h != nil {
+79 -86
View File
@@ -14,14 +14,25 @@ import (
type Ops struct {
// version is incremented at each Reset.
version int
version uint32
// data contains the serialized operations.
data []byte
// refs hold external references for operations.
refs []interface{}
// stringRefs provides space for string references, pointers to which will
// be stored in refs. Storing a string directly in refs would cause a heap
// allocation, to store the string header in an interface value. The backing
// array of stringRefs, on the other hand, gets reused between calls to
// reset, making string references free on average.
//
// Appending to stringRefs might reallocate the backing array, which will
// leave pointers to the old array in refs. This temporarily causes a slight
// increase in memory usage, but this, too, amortizes away as the capacity
// of stringRefs approaches its stable maximum.
stringRefs []string
// nextStateID is the id allocated for the next
// StateOp.
nextStateID int
nextStateID uint32
// multipOp indicates a multi-op such as clip.Path is being added.
multipOp bool
@@ -40,31 +51,23 @@ const (
TypeMacro OpType = iota + firstOpIndex
TypeCall
TypeDefer
TypePushTransform
TypeTransform
TypePopTransform
TypeInvalidate
TypePushOpacity
TypePopOpacity
TypeImage
TypePaint
TypeColor
TypeLinearGradient
TypePass
TypePopPass
TypePointerInput
TypeClipboardRead
TypeClipboardWrite
TypeSource
TypeTarget
TypeOffer
TypeKeyInput
TypeKeyFocus
TypeKeySoftKeyboard
TypeInput
TypeKeyInputHint
TypeSave
TypeLoad
TypeAux
TypeClip
TypePopClip
TypeProfile
TypeCursor
TypePath
TypeStroke
@@ -72,30 +75,28 @@ const (
TypeSemanticDesc
TypeSemanticClass
TypeSemanticSelected
TypeSemanticDisabled
TypeSnippet
TypeSelection
TypeSemanticEnabled
TypeActionInput
)
type StackID struct {
id int
prev int
id uint32
prev uint32
}
// StateOp represents a saved operation snapshot to be restored
// later.
type StateOp struct {
id int
macroID int
id uint32
macroID uint32
ops *Ops
}
// stack tracks the integer identities of stack operations to ensure correct
// pairing of their push and pop methods.
type stack struct {
currentID int
nextID int
currentID uint32
nextID uint32
}
type StackKind uint8
@@ -111,6 +112,7 @@ const (
ClipStack StackKind = iota
TransStack
PassStack
OpacityStack
_StackKind
)
@@ -124,31 +126,24 @@ const (
TypeMacroLen = 1 + 4 + 4
TypeCallLen = 1 + 4 + 4 + 4 + 4
TypeDeferLen = 1
TypePushTransformLen = 1 + 4*6
TypeTransformLen = 1 + 1 + 4*6
TypePopTransformLen = 1
TypePushOpacityLen = 1 + 4
TypePopOpacityLen = 1
TypeRedrawLen = 1 + 8
TypeImageLen = 1
TypeImageLen = 1 + 1
TypePaintLen = 1
TypeColorLen = 1 + 4
TypeLinearGradientLen = 1 + 8*2 + 4*2
TypePassLen = 1
TypePopPassLen = 1
TypePointerInputLen = 1 + 1 + 1*2 + 2*4 + 2*4
TypeClipboardReadLen = 1
TypeClipboardWriteLen = 1
TypeSourceLen = 1
TypeTargetLen = 1
TypeOfferLen = 1
TypeKeyInputLen = 1 + 1
TypeKeyFocusLen = 1 + 1
TypeKeySoftKeyboardLen = 1 + 1
TypeInputLen = 1
TypeKeyInputHintLen = 1 + 1
TypeSaveLen = 1 + 4
TypeLoadLen = 1 + 4
TypeAuxLen = 1
TypeClipLen = 1 + 4*4 + 1 + 1
TypePopClipLen = 1
TypeProfileLen = 1
TypeCursorLen = 2
TypePathLen = 8 + 1
TypeStrokeLen = 1 + 4
@@ -156,9 +151,7 @@ const (
TypeSemanticDescLen = 1
TypeSemanticClassLen = 2
TypeSemanticSelectedLen = 2
TypeSemanticDisabledLen = 2
TypeSnippetLen = 1 + 4 + 4
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
TypeSemanticEnabledLen = 2
TypeActionInputLen = 1 + 1
)
@@ -183,8 +176,12 @@ func Reset(o *Ops) {
for i := range o.refs {
o.refs[i] = nil
}
for i := range o.stringRefs {
o.stringRefs[i] = ""
}
o.data = o.data[:0]
o.refs = o.refs[:0]
o.stringRefs = o.stringRefs[:0]
o.nextStateID = 0
o.version++
}
@@ -248,11 +245,11 @@ func AddCall(o *Ops, callOps *Ops, pc PC, end PC) {
bo.PutUint32(data[13:], uint32(end.refs))
}
func PushOp(o *Ops, kind StackKind) (StackID, int) {
func PushOp(o *Ops, kind StackKind) (StackID, uint32) {
return o.stacks[kind].push(), o.macroStack.currentID
}
func PopOp(o *Ops, kind StackKind, sid StackID, macroID int) {
func PopOp(o *Ops, kind StackKind, sid StackID, macroID uint32) {
if o.macroStack.currentID != macroID {
panic("stack push and pop must not cross macro boundary")
}
@@ -265,12 +262,26 @@ func Write1(o *Ops, n int, ref1 interface{}) []byte {
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 {
o.data = append(o.data, make([]byte, n)...)
o.refs = append(o.refs, ref1, ref2)
return o.data[len(o.data)-n:]
}
func Write2String(o *Ops, n int, ref1 interface{}, ref2 string) []byte {
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 {
o.data = append(o.data, make([]byte, n)...)
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 {
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 {
@@ -354,6 +365,14 @@ func DecodeTransform(data []byte) (t f32.Affine2D, push bool) {
return f32.NewAffine2D(a, b, c, d, e, f), push
}
func DecodeOpacity(data []byte) float32 {
if OpType(data[0]) != TypePushOpacity {
panic("invalid op")
}
bo := binary.LittleEndian
return math.Float32frombits(bo.Uint32(data[1:]))
}
// DecodeSave decodes the state id of a save op.
func DecodeSave(data []byte) int {
if OpType(data[0]) != TypeSave {
@@ -381,31 +400,23 @@ var opProps = [0x100]opProp{
TypeMacro: {Size: TypeMacroLen, NumRefs: 0},
TypeCall: {Size: TypeCallLen, NumRefs: 1},
TypeDefer: {Size: TypeDeferLen, NumRefs: 0},
TypePushTransform: {Size: TypePushTransformLen, NumRefs: 0},
TypeTransform: {Size: TypeTransformLen, 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},
TypePaint: {Size: TypePaintLen, NumRefs: 0},
TypeColor: {Size: TypeColorLen, NumRefs: 0},
TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0},
TypePass: {Size: TypePassLen, NumRefs: 0},
TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1},
TypeClipboardRead: {Size: TypeClipboardReadLen, NumRefs: 1},
TypeClipboardWrite: {Size: TypeClipboardWriteLen, NumRefs: 1},
TypeSource: {Size: TypeSourceLen, NumRefs: 2},
TypeTarget: {Size: TypeTargetLen, NumRefs: 2},
TypeOffer: {Size: TypeOfferLen, NumRefs: 3},
TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2},
TypeKeyFocus: {Size: TypeKeyFocusLen, NumRefs: 1},
TypeKeySoftKeyboard: {Size: TypeKeySoftKeyboardLen, NumRefs: 0},
TypeInput: {Size: TypeInputLen, NumRefs: 1},
TypeKeyInputHint: {Size: TypeKeyInputHintLen, NumRefs: 1},
TypeSave: {Size: TypeSaveLen, NumRefs: 0},
TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
TypeAux: {Size: TypeAuxLen, NumRefs: 0},
TypeClip: {Size: TypeClipLen, NumRefs: 0},
TypePopClip: {Size: TypePopClipLen, NumRefs: 0},
TypeProfile: {Size: TypeProfileLen, NumRefs: 1},
TypeCursor: {Size: TypeCursorLen, NumRefs: 0},
TypePath: {Size: TypePathLen, NumRefs: 0},
TypeStroke: {Size: TypeStrokeLen, NumRefs: 0},
@@ -413,23 +424,21 @@ var opProps = [0x100]opProp{
TypeSemanticDesc: {Size: TypeSemanticDescLen, NumRefs: 1},
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
TypeSemanticDisabled: {Size: TypeSemanticDisabledLen, NumRefs: 0},
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0},
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
}
func (t OpType) props() (size, numRefs int) {
func (t OpType) props() (size, numRefs uint32) {
v := opProps[t]
return int(v.Size), int(v.NumRefs)
return uint32(v.Size), uint32(v.NumRefs)
}
func (t OpType) Size() int {
return int(opProps[t].Size)
func (t OpType) Size() uint32 {
return uint32(opProps[t].Size)
}
func (t OpType) NumRefs() int {
return int(opProps[t].NumRefs)
func (t OpType) NumRefs() uint32 {
return uint32(opProps[t].NumRefs)
}
func (t OpType) String() string {
@@ -440,14 +449,14 @@ func (t OpType) String() string {
return "Call"
case TypeDefer:
return "Defer"
case TypePushTransform:
return "PushTransform"
case TypeTransform:
return "Transform"
case TypePopTransform:
return "PopTransform"
case TypeInvalidate:
return "Invalidate"
case TypePushOpacity:
return "PushOpacity"
case TypePopOpacity:
return "PopOpacity"
case TypeImage:
return "Image"
case TypePaint:
@@ -460,24 +469,10 @@ func (t OpType) String() string {
return "Pass"
case TypePopPass:
return "PopPass"
case TypePointerInput:
return "PointerInput"
case TypeClipboardRead:
return "ClipboardRead"
case TypeClipboardWrite:
return "ClipboardWrite"
case TypeSource:
return "Source"
case TypeTarget:
return "Target"
case TypeOffer:
return "Offer"
case TypeKeyInput:
return "KeyInput"
case TypeKeyFocus:
return "KeyFocus"
case TypeKeySoftKeyboard:
return "KeySoftKeyboard"
case TypeInput:
return "Input"
case TypeKeyInputHint:
return "KeyInputHint"
case TypeSave:
return "Save"
case TypeLoad:
@@ -488,8 +483,6 @@ func (t OpType) String() string {
return "Clip"
case TypePopClip:
return "PopClip"
case TypeProfile:
return "Profile"
case TypeCursor:
return "Cursor"
case TypePath:
+13 -13
View File
@@ -26,8 +26,8 @@ type EncodedOp struct {
// Key is a unique key for a given op.
type Key struct {
ops *Ops
pc int
version int
pc uint32
version uint32
}
// Shadow of op.MacroOp.
@@ -39,8 +39,8 @@ type macroOp struct {
// PC is an instruction counter for an operation list.
type PC struct {
data int
refs int
data uint32
refs uint32
}
type macro struct {
@@ -128,7 +128,7 @@ func (r *Reader) Decode() (EncodedOp, bool) {
if nrefs != 1 {
panic("internal error: unexpected number of macro refs")
}
deferData := Write1(&r.deferOps, n, refs[0])
deferData := Write1(&r.deferOps, int(n), refs[0])
copy(deferData, data)
r.pc.data += n
r.pc.refs += nrefs
@@ -154,8 +154,8 @@ func (r *Reader) Decode() (EncodedOp, bool) {
r.pc = op.endpc
} else {
// Treat an incomplete macro as containing all remaining ops.
r.pc.data = len(r.ops.data)
r.pc.refs = len(r.ops.refs)
r.pc.data = uint32(len(r.ops.data))
r.pc.refs = uint32(len(r.ops.refs))
}
continue
}
@@ -171,8 +171,8 @@ func (op *opMacroDef) decode(data []byte) {
}
bo := binary.LittleEndian
data = data[:TypeMacroLen]
op.endpc.data = int(int32(bo.Uint32(data[1:])))
op.endpc.refs = int(int32(bo.Uint32(data[5:])))
op.endpc.data = bo.Uint32(data[1:])
op.endpc.refs = bo.Uint32(data[5:])
}
func (m *macroOp) decode(data []byte, refs []interface{}) {
@@ -183,8 +183,8 @@ func (m *macroOp) decode(data []byte, refs []interface{}) {
data = data[:TypeCallLen]
m.ops = refs[0].(*Ops)
m.start.data = int(int32(bo.Uint32(data[1:])))
m.start.refs = int(int32(bo.Uint32(data[5:])))
m.end.data = int(int32(bo.Uint32(data[9:])))
m.end.refs = int(int32(bo.Uint32(data[13:])))
m.start.data = bo.Uint32(data[1:])
m.start.refs = bo.Uint32(data[5:])
m.end.data = bo.Uint32(data[9:])
m.end.refs = bo.Uint32(data[13:])
}
+11 -24
View File
@@ -3,35 +3,22 @@
package clipboard
import (
"gioui.org/internal/ops"
"io"
"gioui.org/io/event"
"gioui.org/op"
)
// Event is generated when the clipboard content is requested.
type Event struct {
Text string
// WriteCmd copies Text to the clipboard.
type WriteCmd struct {
Type string
Data io.ReadCloser
}
// ReadOp requests the text of the clipboard, delivered to
// the current handler through an Event.
type ReadOp struct {
// ReadCmd requests the text of the clipboard, delivered to
// the handler through an [io/transfer.DataEvent].
type ReadCmd struct {
Tag event.Tag
}
// WriteOp copies Text to the clipboard.
type WriteOp struct {
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() {}
func (WriteCmd) ImplementsCommand() {}
func (ReadCmd) ImplementsCommand() {}
+20 -34
View File
@@ -1,41 +1,12 @@
// SPDX-License-Identifier: Unlicense OR MIT
/*
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 contains types for event handling.
package event
// Queue maps an event handler key to the events
// available to the handler.
type Queue interface {
// Events returns the available events for an
// event handler tag.
Events(t Tag) []Event
}
import (
"gioui.org/internal/ops"
"gioui.org/op"
)
// Tag is the stable identifier for an event handler.
// For a handler h, the tag is typically &h.
@@ -45,3 +16,18 @@ type Tag interface{}
type Event interface {
ImplementsEvent()
}
// Filter represents a filter for [Event] types.
type Filter interface {
ImplementsFilter()
}
// Op declares a tag for input routing at the current transformation
// and clip area hierarchy. It panics if tag is nil.
func Op(o *op.Ops, tag Tag) {
if tag == nil {
panic("Tag must be non-nil")
}
data := ops.Write1(&o.Internal, ops.TypeInputLen, tag)
data[0] = byte(ops.TypeInput)
}
+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")
}
}
+331
View File
@@ -0,0 +1,331 @@
// 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,
ScrollBounds: image.Rect(-100, -100, 100, 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)
}
}
+348 -295
View File
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: Unlicense OR MIT
package router
package input
import (
"image"
@@ -10,7 +10,6 @@ import (
f32internal "gioui.org/internal/f32"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/io/system"
@@ -18,14 +17,8 @@ import (
)
type pointerQueue struct {
hitTree []hitNode
areas []areaNode
cursor pointer.Cursor
handlers map[event.Tag]*pointerHandler
pointers []pointerInfo
transfers []io.ReadCloser // pending data transfers
scratch []event.Tag
hitTree []hitNode
areas []areaNode
semantic struct {
idsAssigned bool
@@ -43,10 +36,15 @@ type hitNode struct {
// For handler nodes.
tag event.Tag
ktag event.Tag
pass bool
}
// pointerState is the input state related to pointer events.
type pointerState struct {
cursor pointer.Cursor
pointers []pointerInfo
}
type pointerInfo struct {
id pointer.ID
pressed bool
@@ -63,17 +61,21 @@ type pointerInfo struct {
}
type pointerHandler struct {
area int
active bool
wantsGrab bool
types pointer.Type
// areaPlusOne is the index into the list of pointerQueue.areas, plus 1.
areaPlusOne int
// setup tracks whether the handler has received
// the pointer.Cancel event that resets its state.
setup bool
}
// pointerFilter represents the union of a set of pointer filters.
type pointerFilter struct {
kinds pointer.Kind
// min and max horizontal/vertical scroll
scrollRange image.Rectangle
sourceMimes []string
targetMimes []string
offeredMime string
data io.ReadCloser
}
type areaOp struct {
@@ -229,33 +231,18 @@ func (c *pointerCollector) addHitNode(n hitNode) {
}
// 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()
c.addHitNode(hitNode{
area: areaID,
tag: tag,
pass: c.state.pass > 0,
})
h, ok := c.q.handlers[tag]
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
state.areaPlusOne = areaID + 1
}
func (c *pointerCollector) keyInputOp(op key.InputOp) {
areaID := c.currentArea()
c.addHitNode(hitNode{
area: areaID,
ktag: op.Tag,
pass: true,
})
func (s *pointerHandler) Reset() {
s.areaPlusOne = 0
}
func (c *pointerCollector) actionInputOp(act system.Action) {
@@ -264,21 +251,109 @@ func (c *pointerCollector) actionInputOp(act system.Action) {
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()
area := &c.q.areas[areaID]
area.semantic.content.tag = op.Tag
if op.Types&(pointer.Press|pointer.Release) != 0 {
area.semantic.content.gestures |= ClickGesture
area.semantic.content.tag = tag
c.newHandler(tag, state)
}
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.scrollRange = p.scrollRange.Union(f.ScrollBounds)
}
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
h := c.newHandler(op.Tag, events)
h.wantsGrab = h.wantsGrab || op.Grab
h.types = h.types | op.Types
h.scrollRange = op.ScrollBounds
return false
}
func (p *pointerFilter) Merge(p2 pointerFilter) {
p.kinds = p.kinds | p2.kinds
p.scrollRange = p.scrollRange.Union(p2.scrollRange)
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.scrollRange.Min.X, p.scrollRange.Max.X)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollRange.Min.Y, p.scrollRange.Max.Y)
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) {
@@ -309,11 +384,11 @@ func (c *pointerCollector) semanticSelected(selected bool) {
area.semantic.content.selected = selected
}
func (c *pointerCollector) semanticDisabled(disabled bool) {
func (c *pointerCollector) semanticEnabled(enabled bool) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.valid = true
area.semantic.content.disabled = disabled
area.semantic.content.disabled = !enabled
}
func (c *pointerCollector) cursor(cursor pointer.Cursor) {
@@ -322,23 +397,28 @@ func (c *pointerCollector) cursor(cursor pointer.Cursor) {
area.cursor = cursor
}
func (c *pointerCollector) sourceOp(op transfer.SourceOp, events *handlerEvents) {
h := c.newHandler(op.Tag, events)
h.sourceMimes = append(h.sourceMimes, op.Type)
func (q *pointerQueue) offerData(handlers map[event.Tag]*handler, state pointerState, req transfer.OfferCmd) (pointerState, []taggedEvent) {
var evts []taggedEvent
for i, p := range state.pointers {
if p.dataSource != req.Tag {
continue
}
if p.dataTarget != nil {
evts = append(evts, taggedEvent{tag: p.dataTarget, event: transfer.DataEvent{
Type: req.Type,
Open: func() io.ReadCloser {
return req.Data
},
}})
}
state.pointers = 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) {
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() {
func (c *pointerCollector) Reset() {
c.q.reset()
c.resetState()
c.ensureRoot()
@@ -432,39 +512,42 @@ func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
return id.id
}
func (q *pointerQueue) ActionAt(pos f32.Point) (system.Action, bool) {
for i := len(q.hitTree) - 1; i >= 0; i-- {
n := &q.hitTree[i]
hit, _ := q.hit(n.area, pos)
if !hit {
continue
}
func (q *pointerQueue) ActionAt(pos f32.Point) (action system.Action, hasAction bool) {
q.hitTest(pos, func(n *hitNode) bool {
area := q.areas[n.area]
return area.action, area.action != 0
}
return 0, false
if area.action != 0 {
action = area.action
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()
for i := len(q.hitTree) - 1; i >= 0; i-- {
n := &q.hitTree[i]
hit, _ := q.hit(n.area, pos)
if !hit {
continue
}
q.hitTest(pos, func(n *hitNode) bool {
area := q.areas[n.area]
if area.semantic.id != 0 {
return area.semantic.id, true
semID = area.semantic.id
hasSemID = true
return false
}
}
return 0, false
return true
})
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.
pass := true
hits := q.scratch[:0]
idx := len(q.hitTree) - 1
cursor := pointer.CursorDefault
for idx >= 0 {
@@ -483,14 +566,11 @@ func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
} else {
idx = n.next
}
if n.tag != nil {
if _, exists := q.handlers[n.tag]; exists {
hits = addHandler(hits, n.tag)
}
if !onNode(n) {
break
}
}
q.scratch = hits[:0]
return hits, cursor
return cursor
}
func (q *pointerQueue) invTransform(areaIdx int, p f32.Point) f32.Point {
@@ -517,17 +597,6 @@ func (q *pointerQueue) hit(areaIdx int, p f32.Point) (bool, pointer.Cursor) {
}
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.areas = q.areas[:0]
q.semantic.idsAssigned = false
@@ -545,80 +614,71 @@ func (q *pointerQueue) reset() {
delete(q.semantic.contentIDs, k)
}
}
for _, rc := range q.transfers {
if rc != nil {
rc.Close()
}
}
q.transfers = nil
}
func (q *pointerQueue) Frame(events *handlerEvents) {
for k, h := range q.handlers {
if !h.active {
q.dropHandler(nil, k)
delete(q.handlers, k)
}
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
}
}
func (q *pointerQueue) Frame(handlers map[event.Tag]*handler, state pointerState) (pointerState, []taggedEvent) {
for _, h := range handlers {
if h.pointer.areaPlusOne != 0 {
area := &q.areas[h.pointer.areaPlusOne-1]
if h.filter.pointer.kinds&(pointer.Press|pointer.Release) != 0 {
area.semantic.content.gestures |= ClickGesture
}
if h.filter.pointer.kinds&pointer.Scroll != 0 {
area.semantic.content.gestures |= ScrollGesture
}
area.semantic.valid = area.semantic.content.gestures != 0
}
}
for i := range q.pointers {
p := &q.pointers[i]
q.deliverEnterLeaveEvents(p, events, p.last)
q.deliverTransferDataEvent(p, events)
var evts []taggedEvent
for i, p := range state.pointers {
changed := false
p, evts, state.cursor, changed = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, p.last)
if changed {
state.pointers = append([]pointerInfo{}, state.pointers...)
state.pointers[i] = p
}
}
return state, evts
}
func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) {
if events != nil {
events.Add(tag, pointer.Event{Type: pointer.Cancel})
}
for i := range q.pointers {
p := &q.pointers[i]
for i := len(p.handlers) - 1; i >= 0; i-- {
if p.handlers[i] == tag {
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
func dropHandler(state pointerState, tag event.Tag) pointerState {
pointers := state.pointers
state.pointers = nil
for _, p := range pointers {
handlers := p.handlers
p.handlers = nil
for _, h := range handlers {
if h != tag {
p.handlers = append(p.handlers, h)
}
}
for i := len(p.entered) - 1; i >= 0; i-- {
if p.entered[i] == tag {
p.entered = append(p.entered[:i], p.entered[i+1:]...)
entered := p.entered
p.entered = nil
for _, h := range entered {
if h != tag {
p.entered = append(p.entered, h)
}
}
state.pointers = append(state.pointers, p)
}
return state
}
// pointerOf returns the pointerInfo index corresponding to the pointer in e.
func (q *pointerQueue) pointerOf(e pointer.Event) int {
for i, p := range q.pointers {
func (s pointerState) pointerOf(e pointer.Event) (pointerState, int) {
for i, p := range s.pointers {
if p.id == e.PointerID {
return i
return s, i
}
}
q.pointers = append(q.pointers, pointerInfo{id: e.PointerID})
return len(q.pointers) - 1
n := len(s.pointers)
s.pointers = append(s.pointers[:n:n], pointerInfo{id: e.PointerID})
return s, len(s.pointers) - 1
}
// Deliver is like Push, but delivers an event to a particular area.
func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEvents) {
var sx, sy = e.Scroll.X, e.Scroll.Y
func (q *pointerQueue) Deliver(handlers map[event.Tag]*handler, areaIdx int, e pointer.Event) []taggedEvent {
scroll := e.Scroll
idx := len(q.hitTree) - 1
// Locate first potential receiver.
for idx != -1 {
@@ -628,31 +688,28 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
}
idx--
}
var evts []taggedEvent
for idx != -1 {
n := &q.hitTree[idx]
idx = n.next
if n.tag == nil {
continue
}
h := q.handlers[n.tag]
if e.Type&h.types == 0 {
h, ok := handlers[n.tag]
if !ok || !h.filter.pointer.Matches(e) {
continue
}
e := e
if e.Type == pointer.Scroll {
if sx == 0 && sy == 0 {
if e.Kind == pointer.Scroll {
if scroll == (f32.Point{}) {
break
}
// 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)
scroll, e.Scroll = h.filter.pointer.clampScroll(scroll)
}
e.Position = q.invTransform(h.area, e.Position)
events.Add(n.tag, e)
if e.Type != pointer.Scroll {
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
evts = append(evts, taggedEvent{tag: n.tag, event: e})
if e.Kind != pointer.Scroll {
break
}
}
return evts
}
// SemanticArea returns the sematic content for area, and its parent area.
@@ -668,106 +725,129 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
return semanticContent{}, -1
}
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
if e.Type == pointer.Cancel {
q.pointers = q.pointers[:0]
for k := range q.handlers {
q.dropHandler(events, k)
func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState, e pointer.Event) (pointerState, []taggedEvent) {
var evts []taggedEvent
if e.Kind == pointer.Cancel {
for k := range handlers {
evts = append(evts, taggedEvent{
event: pointer.Event{Kind: pointer.Cancel},
tag: k,
})
}
return
state.pointers = nil
return state, evts
}
pidx := q.pointerOf(e)
p := &q.pointers[pidx]
p.last = e
state, pidx := state.pointerOf(e)
p := state.pointers[pidx]
switch e.Type {
switch e.Kind {
case pointer.Press:
q.deliverEnterLeaveEvents(p, events, e)
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
p.pressed = true
q.deliverEvent(p, events, e)
evts = q.deliverEvent(handlers, p, evts, e)
case pointer.Move:
if p.pressed {
e.Type = pointer.Drag
e.Kind = pointer.Drag
}
q.deliverEnterLeaveEvents(p, events, e)
q.deliverEvent(p, events, e)
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
evts = q.deliverEvent(handlers, p, evts, e)
if p.pressed {
q.deliverDragEvent(p, events)
p, evts = q.deliverDragEvent(handlers, p, evts)
}
case pointer.Release:
q.deliverEvent(p, events, e)
evts = q.deliverEvent(handlers, p, evts, e)
p.pressed = false
q.deliverEnterLeaveEvents(p, events, e)
q.deliverDropEvent(p, events)
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
p, evts = q.deliverDropEvent(handlers, p, evts)
case pointer.Scroll:
q.deliverEnterLeaveEvents(p, events, e)
q.deliverEvent(p, events, e)
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
evts = q.deliverEvent(handlers, p, evts, e)
default:
panic("unsupported pointer event type")
}
p.last = e
if !p.pressed && len(p.entered) == 0 {
// No longer need to track pointer.
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
if p.pressed && len(p.handlers) == 1 {
e.Priority = pointer.Grabbed
foremost = false
}
var sx, sy = e.Scroll.X, e.Scroll.Y
scroll := e.Scroll
for _, k := range p.handlers {
h := q.handlers[k]
if e.Type == pointer.Scroll {
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 {
h, ok := handlers[k]
if !ok {
continue
}
f := h.filter.pointer
if !f.Matches(e) {
continue
}
if e.Kind == pointer.Scroll {
if scroll == (f32.Point{}) {
return evts
}
scroll, e.Scroll = f.clampScroll(scroll)
}
e := e
if foremost {
foremost = false
e.Priority = pointer.Foremost
}
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
evts = append(evts, taggedEvent{event: e, tag: k})
}
return evts
}
func (q *pointerQueue) deliverEnterLeaveEvents(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
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.
} else {
hits, q.cursor = q.opHit(e.Position)
if p.pressed {
// Filter out non-participating handlers,
// except potential transfer targets when a transfer has been initiated.
var hitsHaveTarget bool
if p.dataSource != nil {
transferSource := q.handlers[p.dataSource]
for _, hit := range hits {
if _, ok := firstMimeMatch(transferSource, q.handlers[hit]); ok {
hitsHaveTarget = true
break
var transSrc *pointerFilter
if p.dataSource != nil {
transSrc = &handlers[p.dataSource].filter.pointer
}
cursor = q.hitTest(e.Position, func(n *hitNode) bool {
h, ok := handlers[n.tag]
if !ok {
return true
}
add := true
if p.pressed {
add = false
// Filter out non-participating handlers,
// except potential transfer targets when a transfer has been initiated.
if _, found := searchTag(p.handlers, n.tag); found {
add = true
}
if transSrc != nil {
if _, ok := firstMimeMatch(transSrc, &h.filter.pointer); ok {
add = true
}
}
}
for i := len(hits) - 1; i >= 0; i-- {
if _, found := searchTag(p.handlers, hits[i]); !found && !hitsHaveTarget {
hits = append(hits[:i], hits[i+1:]...)
}
if add {
hits = addHandler(hits, n.tag)
}
} else {
p.handlers = append(p.handlers[:0], hits...)
return true
})
if !p.pressed {
changed = true
p.handlers = hits
}
}
// Deliver Leave events.
@@ -775,111 +855,94 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
if _, found := searchTag(hits, k); found {
continue
}
h := q.handlers[k]
e.Type = pointer.Leave
h, ok := handlers[k]
if !ok {
continue
}
changed = true
e := e
e.Kind = pointer.Leave
if e.Type&h.types != 0 {
e := e
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
if h.filter.pointer.Matches(e) {
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
evts = append(evts, taggedEvent{tag: k, event: e})
}
}
// Deliver Enter events.
for _, k := range hits {
h := q.handlers[k]
if _, found := searchTag(p.entered, k); found {
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 {
e := e
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
if h.filter.pointer.Matches(e) {
e.Position = q.invTransform(h.pointer.areaPlusOne-1, e.Position)
evts = append(evts, taggedEvent{tag: k, event: e})
}
}
p.entered = 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 {
return
return p, evts
}
// Identify the data source.
for _, k := range p.entered {
src := q.handlers[k]
src := &handlers[k].filter.pointer
if len(src.sourceMimes) == 0 {
continue
}
// One data source handler per pointer.
p.dataSource = k
// Notify all potential targets.
for k, tgt := range q.handlers {
if _, ok := firstMimeMatch(src, tgt); ok {
events.Add(k, transfer.InitiateEvent{})
for k, tgt := range handlers {
if _, ok := firstMimeMatch(src, &tgt.filter.pointer); ok {
evts = append(evts, taggedEvent{tag: k, event: transfer.InitiateEvent{}})
}
}
break
}
return p, evts
}
func (q *pointerQueue) deliverDropEvent(p *pointerInfo, events *handlerEvents) {
func (q *pointerQueue) deliverDropEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
if p.dataSource == nil {
return
return p, evts
}
// Request data from the source.
src := q.handlers[p.dataSource]
src := &handlers[p.dataSource].filter.pointer
for _, k := range p.entered {
h := q.handlers[k]
if m, ok := firstMimeMatch(src, h); ok {
h := handlers[k]
if m, ok := firstMimeMatch(src, &h.filter.pointer); ok {
p.dataTarget = k
events.Add(p.dataSource, transfer.RequestEvent{Type: m})
return
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.RequestEvent{Type: m}})
return p, evts
}
}
// No valid target found, abort.
q.deliverTransferCancelEvent(p, events)
return q.deliverTransferCancelEvent(handlers, p, evts)
}
func (q *pointerQueue) deliverTransferDataEvent(p *pointerInfo, events *handlerEvents) {
if p.dataSource == nil {
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{})
func (q *pointerQueue) deliverTransferCancelEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent) (pointerInfo, []taggedEvent) {
evts = append(evts, taggedEvent{tag: p.dataSource, event: transfer.CancelEvent{}})
// Cancel all potential targets.
src := q.handlers[p.dataSource]
for k, h := range q.handlers {
if _, ok := firstMimeMatch(src, h); ok {
events.Add(k, transfer.CancelEvent{})
src := &handlers[p.dataSource].filter.pointer
for k, h := range handlers {
if _, ok := firstMimeMatch(src, &h.filter.pointer); ok {
evts = append(evts, taggedEvent{tag: k, event: transfer.CancelEvent{}})
}
}
src.offeredMime = ""
src.data = nil
p.dataSource = nil
p.dataTarget = nil
return p, evts
}
// ClipFor clips r to the parents of area.
@@ -914,7 +977,7 @@ func addHandler(tags []event.Tag, tag event.Tag) []event.Tag {
}
// 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 _, m2 := range src.sourceMimes {
if m1 == m2 {
@@ -951,13 +1014,3 @@ func (a *areaNode) bounds() image.Rectangle {
Max: a.trans.Transform(f32internal.FPt(a.area.rect.Max)),
}.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
+882
View File
@@ -0,0 +1,882 @@
// 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
processedFilter 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
}
}
}
if !q.deferring {
for i := range q.changes {
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)
}
q.key.processedFilter = append(q.key.processedFilter, q.key.scratchFilter...)
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 i := 1; i <= idx; i++ {
first.events = append(first.events, q.changes[i].events...)
}
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
}
// 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) {
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
package router
package input
import (
"fmt"
@@ -9,6 +9,7 @@ import (
"testing"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/op"
@@ -74,13 +75,19 @@ func TestSemanticTree(t *testing.T) {
func TestSemanticDescription(t *testing.T) {
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.LabelOp("label").Add(&ops)
semantic.Button.Add(&ops)
semantic.DisabledOp(true).Add(&ops)
semantic.EnabledOp(false).Add(&ops)
semantic.SelectedOp(true).Add(&ops)
var r Router
events(&r, -1, pointer.Filter{
Target: h,
Kinds: pointer.Press | pointer.Release,
})
r.Frame(&ops)
tree := r.AppendSemantics(nil)
got := tree[0].Desc
+105 -220
View File
@@ -1,18 +1,9 @@
// SPDX-License-Identifier: Unlicense OR MIT
/*
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 implements key and text events and operations.
package key
import (
"encoding/binary"
"fmt"
"math"
"strings"
"gioui.org/f32"
@@ -21,59 +12,40 @@ import (
"gioui.org/op"
)
// InputOp declares a handler ready for key events.
// Key events are in general only delivered to the
// focused key handler.
type InputOp struct {
Tag event.Tag
// Hint describes the type of text expected by Tag.
Hint InputHint
// Keys is the set of keys Tag can handle. That is, Tag will only
// receive an Event if its key and modifiers are accepted by Keys.Contains.
// As a special case, the topmost (first added) InputOp handler receives all
// unhandled events.
Keys Set
// Filter matches any [Event] that matches the parameters.
type Filter struct {
// Focus is the tag that must be focused for the filter to match. It has no effect
// if it is nil.
Focus event.Tag
// Required is the set of modifiers that must be included in events matched.
Required Modifiers
// Optional is the set of modifiers that may be included in events matched.
Optional Modifiers
// Name of the key to be matched. As a special case, the empty
// Name matches every key not matched by any other filter.
Name Name
}
// Set is an expression that describes a set of key combinations, in the form
// "<modifiers>-<keyset>|...". Modifiers are separated by dashes, optional
// modifiers are enclosed by parentheses. A key set is either a literal key
// name or a list of key names separated by commas and enclosed in brackets.
//
// 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
// InputHintOp describes the type of text expected by a tag.
type InputHintOp struct {
Tag event.Tag
Hint InputHint
}
// SoftKeyboardOp shows or hide the on-screen keyboard, if available.
// It replaces any previous SoftKeyboardOp.
type SoftKeyboardOp struct {
// SoftKeyboardCmd shows or hides the on-screen keyboard, if available.
type SoftKeyboardCmd struct {
Show bool
}
// FocusOp sets or clears the keyboard focus. It replaces any previous
// FocusOp in the same frame.
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 {
// SelectionCmd updates the selection for an input handler.
type SelectionCmd struct {
Tag event.Tag
Range
Caret
}
// SnippetOp updates the content snippet for an input handler.
type SnippetOp struct {
// SnippetCmd updates the content snippet for an input handler.
type SnippetCmd struct {
Tag event.Tag
Snippet
}
@@ -118,11 +90,8 @@ type FocusEvent struct {
// An Event is generated when a key is pressed. For text input
// use EditEvent.
type Event struct {
// Name of the 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.
Name string
// Name of the key.
Name Name
// Modifiers is the set of active modifiers when the key was pressed.
Modifiers Modifiers
// State is the state of the key when the event was fired.
@@ -136,6 +105,13 @@ type EditEvent struct {
Text string
}
// FocusFilter matches any [FocusEvent], [EditEvent], [SnippetEvent],
// or [SelectionEvent] with the specified target.
type FocusFilter struct {
// Target is a tag specified in a previous event.Op.
Target event.Tag
}
// InputHint changes the on-screen-keyboard type. That hints the
// type of data that might be entered by the user.
type InputHint uint8
@@ -189,41 +165,60 @@ const (
ModSuper
)
// Name is the identifier for a keyboard key.
//
// For letters, the upper case form is used, via unicode.ToUpper.
// The shift modifier is taken into account, all other
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
// combinations both give the Name "!" with the US keyboard layout.
type Name string
const (
// Names for special keys.
NameLeftArrow = "←"
NameRightArrow = "→"
NameUpArrow = "↑"
NameDownArrow = "↓"
NameReturn = "⏎"
NameEnter = "⌤"
NameEscape = "⎋"
NameHome = "⇱"
NameEnd = "⇲"
NameDeleteBackward = "⌫"
NameDeleteForward = "⌦"
NamePageUp = "⇞"
NamePageDown = "⇟"
NameTab = "Tab"
NameSpace = "Space"
NameCtrl = "Ctrl"
NameShift = "Shift"
NameAlt = "Alt"
NameSuper = "Super"
NameCommand = "⌘"
NameF1 = "F1"
NameF2 = "F2"
NameF3 = "F3"
NameF4 = "F4"
NameF5 = "F5"
NameF6 = "F6"
NameF7 = "F7"
NameF8 = "F8"
NameF9 = "F9"
NameF10 = "F10"
NameF11 = "F11"
NameF12 = "F12"
NameBack = "Back"
NameLeftArrow Name = "←"
NameRightArrow Name = "→"
NameUpArrow Name = "↑"
NameDownArrow Name = "↓"
NameReturn Name = "⏎"
NameEnter Name = "⌤"
NameEscape Name = "⎋"
NameHome Name = "⇱"
NameEnd Name = "⇲"
NameDeleteBackward Name = "⌫"
NameDeleteForward Name = "⌦"
NamePageUp Name = "⇞"
NamePageDown Name = "⇟"
NameTab Name = "Tab"
NameSpace Name = "Space"
NameCtrl Name = "Ctrl"
NameShift Name = "Shift"
NameAlt Name = "Alt"
NameSuper Name = "Super"
NameCommand Name = "⌘"
NameF1 Name = "F1"
NameF2 Name = "F2"
NameF3 Name = "F3"
NameF4 Name = "F4"
NameF5 Name = "F5"
NameF6 Name = "F6"
NameF7 Name = "F7"
NameF8 Name = "F8"
NameF9 Name = "F9"
NameF10 Name = "F10"
NameF11 Name = "F11"
NameF12 Name = "F12"
NameBack Name = "Back"
)
type FocusDirection int
const (
FocusRight FocusDirection = iota
FocusLeft
FocusUp
FocusDown
FocusForward
FocusBackward
)
// Contain reports whether m contains all modifiers
@@ -232,162 +227,52 @@ func (m Modifiers) Contain(m2 Modifiers) bool {
return m&m2 == m2
}
func (k Set) Contains(name string, mods Modifiers) bool {
ks := string(k)
for len(ks) > 0 {
// Cut next key expression.
chord, rest, _ := cut(ks, "|")
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
// FocusCmd requests to set or clear the keyboard focus.
type FocusCmd struct {
// Tag is the new focus. The focus is cleared if Tag is nil, or if Tag
// has no [event.Op] references.
Tag event.Tag
}
func keySetContains(keySet, name string) bool {
// 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) {
func (h InputHintOp) Add(o *op.Ops) {
if h.Tag == nil {
panic("Tag must be non-nil")
}
filter := h.Keys
data := ops.Write2(&o.Internal, ops.TypeKeyInputLen, h.Tag, &filter)
data[0] = byte(ops.TypeKeyInput)
data := ops.Write1(&o.Internal, ops.TypeKeyInputHintLen, h.Tag)
data[0] = byte(ops.TypeKeyInputHint)
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 (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {}
func (e Event) String() string {
return fmt.Sprintf("%v %v %v}", e.Name, e.Modifiers, e.State)
}
func (FocusCmd) ImplementsCommand() {}
func (SoftKeyboardCmd) ImplementsCommand() {}
func (SelectionCmd) ImplementsCommand() {}
func (SnippetCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
func (FocusFilter) ImplementsFilter() {}
func (m Modifiers) String() string {
var strs []string
if m.Contain(ModCtrl) {
strs = append(strs, NameCtrl)
strs = append(strs, string(NameCtrl))
}
if m.Contain(ModCommand) {
strs = append(strs, NameCommand)
strs = append(strs, string(NameCommand))
}
if m.Contain(ModShift) {
strs = append(strs, NameShift)
strs = append(strs, string(NameShift))
}
if m.Contain(ModAlt) {
strs = append(strs, NameAlt)
strs = append(strs, string(NameAlt))
}
if m.Contain(ModSuper) {
strs = append(strs, NameSuper)
strs = append(strs, string(NameSuper))
}
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
object such as a finger.
The InputOp operation is used to declare a handler ready for pointer
events. Use an event.Queue to receive 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.
The [event.Op] operation is used to declare a handler ready for pointer
events.
# Hit areas
Clip operations from package op/clip are used for specifying
hit areas where subsequent InputOps are active.
Clip operations from package [op/clip] are used for specifying
hit areas where handlers may receive events.
For example, to set up a handler with a rectangular hit area:
r := image.Rectangle{...}
area := clip.Rect(r).Push(ops)
pointer.InputOp{Tag: h}.Add(ops)
event.Op{Tag: h}.Add(ops)
area.Pop()
Note that hit areas behave similar to painting: the effective area of a stack
@@ -54,11 +37,11 @@ For example:
var h1, h2 *Handler
area := clip.Rect(...).Push(ops)
pointer.InputOp{Tag: h1}.Add(Ops)
event.Op{Tag: h1}.Add(Ops)
area.Pop()
area := clip.Rect(...).Push(ops)
pointer.InputOp{Tag: h2}.Add(ops)
event.Op{Tag: h2}.Add(ops)
area.Pop()
implies a tree of two inner nodes, each with one pointer handler attached.
+29 -45
View File
@@ -3,8 +3,6 @@
package pointer
import (
"encoding/binary"
"fmt"
"image"
"strings"
"time"
@@ -18,7 +16,7 @@ import (
// Event is a pointer event.
type Event struct {
Type Type
Kind Kind
Source Source
// PointerID is the id for the pointer and can be used
// to track a particular pointer from Press to
@@ -32,8 +30,10 @@ type Event struct {
Time time.Duration
// Buttons are the set of pressed mouse buttons for this event.
Buttons Buttons
// Position is the position of the event, relative to
// the current transformation, as set by op.TransformOp.
// Position is the coordinates of the event in the local coordinate
// system of the receiving tag. The transformation from global window
// coordinates to local coordinates is performed by the inverse of
// the effective transformation of the tag.
Position f32.Point
// Scroll is the scroll amount, if any.
Scroll f32.Point
@@ -51,18 +51,16 @@ type PassOp struct {
type PassStack struct {
ops *ops.Ops
id ops.StackID
macroID int
macroID uint32
}
// InputOp declares an input handler ready for pointer
// events.
type InputOp struct {
Tag event.Tag
// Grab, if set, request that the handler get
// Grabbed priority.
Grab bool
// Types is a bitwise-or of event types to receive.
Types Type
// Filter matches every [Event] that target the Tag and whose kind is
// included in Kinds. Note that only tags specified in [event.Op] can
// be targeted by pointer events.
type Filter struct {
Target event.Tag
// Kinds is a bitwise-or of event types to match.
Kinds Kind
// ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy
//
@@ -71,10 +69,16 @@ type InputOp struct {
ScrollBounds image.Rectangle
}
// GrabCmd requests a pointer grab on the pointer identified by ID.
type GrabCmd struct {
Tag event.Tag
ID ID
}
type ID uint16
// Type of an Event.
type Type uint
// Kind of an Event.
type Kind uint
// Priority of an Event.
type Priority uint8
@@ -169,7 +173,7 @@ const (
const (
// A Cancel event is generated when the current gesture is
// interrupted by other handlers or the system.
Cancel Type = (1 << iota) >> 1
Cancel Kind = 1 << iota
// Press of a pointer.
Press
// Release of a pointer.
@@ -235,36 +239,12 @@ func (op Cursor) Add(o *op.Ops) {
data[1] = byte(op)
}
// Add panics if the scroll range does not contain zero.
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 {
func (t Kind) String() string {
if t == Cancel {
return "Cancel"
}
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 buf.Len() > 0 {
buf.WriteByte('|')
@@ -275,7 +255,7 @@ func (t Type) String() string {
return buf.String()
}
func (t Type) string() string {
func (t Kind) string() string {
switch t {
case Press:
return "Press"
@@ -402,3 +382,7 @@ func (c Cursor) String() string {
}
func (Event) ImplementsEvent() {}
func (GrabCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
func TestTypeString(t *testing.T) {
for _, tc := range []struct {
typ Type
typ Kind
res string
}{
{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.
type SelectedOp bool
// DisabledOp describes the disabled state.
type DisabledOp bool
// EnabledOp describes the enabled state.
type EnabledOp bool
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)
}
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)
}
@@ -63,10 +63,10 @@ func (s SelectedOp) Add(o *op.Ops) {
}
}
func (d DisabledOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeSemanticDisabledLen)
data[0] = byte(ops.TypeSemanticDisabled)
if d {
func (e EnabledOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeSemanticEnabledLen)
data[0] = byte(ops.TypeSemanticEnabled)
if e {
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:
//
// - Data sources are registered with SourceOps, data targets with TargetOps.
// - A data source receives a RequestEvent when a transfer is initiated.
// It must respond with an OfferOp.
// - The target receives a DataEvent when transferring to it. It must close
// the event data after use.
// - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag
// is initiated, and an [RequestEvent] for each initiation of a data transfer.
// Sources respond to requests with [OfferCmd].
// - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data.
// The target must close the data event after use.
//
// When a user initiates a pointer-guided drag and drop transfer, the
// source as well as all potential targets receive an InitiateEvent.
@@ -20,29 +20,11 @@ package transfer
import (
"io"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/op"
)
// SourceOp registers a tag as a data source for a MIME type.
// Use multiple SourceOps if a tag supports multiple types.
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 {
// OfferCmd is used by data sources as a response to a RequestEvent.
type OfferCmd struct {
Tag event.Tag
// Type is the MIME type of Data.
// It must be the Type from the corresponding RequestEvent.
@@ -50,32 +32,33 @@ type OfferOp struct {
// Data contains the offered data. It is closed when the
// transfer is complete or cancelled.
// Data must be kept valid until closed, and it may be used from
// a goroutine separate from the one processing the frame..
// a goroutine separate from the one processing the frame.
Data io.ReadCloser
}
func (op SourceOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type)
data[0] = byte(ops.TypeSource)
func (OfferCmd) ImplementsCommand() {}
// SourceFilter filters for any [RequestEvent] that match a MIME type
// as well as [InitiateEvent] and [CancelEvent].
// Use multiple filters to offer multiple types.
type SourceFilter struct {
// Target is a tag included in a previous event.Op.
Target event.Tag
// Type is the MIME type supported by this source.
Type string
}
func (op TargetOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeTargetLen, op.Tag, op.Type)
data[0] = byte(ops.TypeTarget)
}
// Add the offer to the list of operations.
// It panics if the Data field is not set.
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)
// TargetFilter filters for any [DataEvent] whose type matches a MIME type
// as well as [CancelEvent]. Use multiple filters to accept multiple types.
type TargetFilter struct {
// Target is a tag included in a previous event.Op.
Target event.Tag
// Type is the MIME type accepted by this target.
Type string
}
// RequestEvent requests data from a data source. The source must
// respond with an OfferOp.
// respond with an OfferCmd.
type RequestEvent struct {
// Type is the first matched type between the source and the target.
Type string
@@ -107,3 +90,6 @@ type DataEvent struct {
}
func (DataEvent) ImplementsEvent() {}
func (SourceFilter) ImplementsFilter() {}
func (TargetFilter) ImplementsFilter() {}
+5 -57
View File
@@ -3,10 +3,9 @@
package layout
import (
"image"
"time"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/system"
"gioui.org/op"
"gioui.org/unit"
@@ -21,9 +20,6 @@ type Context struct {
Constraints Constraints
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 time.Time
@@ -32,46 +28,10 @@ type Context struct {
// Interested users must look up and populate these values manually.
Locale system.Locale
input.Source
*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.
func (c Context) Dp(v unit.Dp) int {
return c.Metric.Dp(v)
@@ -82,21 +42,9 @@ func (c Context) Sp(v unit.Sp) int {
return c.Metric.Sp(v)
}
// Events returns the events available for the key. If no
// queue is configured, Events returns nil.
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.
// Disabled returns a copy of this context with a disabled Source,
// blocking widgets from changing its state and receiving events.
func (c Context) Disabled() Context {
c.Queue = nil
c.Source = input.Source{}
return c
}
+24
View File
@@ -103,6 +103,30 @@ func ExampleStack() {
// Expand: {(50,50) (100,100)}
}
func ExampleBackground() {
gtx := layout.Context{
Ops: new(op.Ops),
Constraints: layout.Constraints{
Max: image.Point{X: 100, Y: 100},
},
}
layout.Background{}.Layout(gtx,
// Force widget to the same size as the second.
func(gtx layout.Context) layout.Dimensions {
fmt.Printf("Expand: %v\n", gtx.Constraints)
return layoutWidget(gtx, 10, 10)
},
// Rigid 50x50 widget.
func(gtx layout.Context) layout.Dimensions {
return layoutWidget(gtx, 50, 50)
},
)
// Output:
// Expand: {(50,50) (100,100)}
}
func ExampleList() {
gtx := layout.Context{
Ops: new(op.Ops),
+20 -20
View File
@@ -144,7 +144,25 @@ func (l *List) Dragging() bool {
}
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
}
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
}
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange)
l.scrollDelta = d
l.Position.Offset += d
}
@@ -332,25 +350,7 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
call := macro.Stop()
defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop()
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
}
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
}
l.scroll.Add(ops, scrollRange)
l.scroll.Add(ops)
call.Add(ops)
return Dimensions{Size: dims}
+12 -12
View File
@@ -8,8 +8,8 @@ import (
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/pointer"
"gioui.org/io/router"
"gioui.org/op"
)
@@ -64,13 +64,13 @@ func TestListScrollToEnd(t *testing.T) {
func TestListPosition(t *testing.T) {
_s := func(e ...event.Event) []event.Event { return e }
r := new(router.Router)
r := new(input.Router)
gtx := Context{
Ops: new(op.Ops),
Constraints: Constraints{
Max: image.Pt(20, 10),
},
Queue: r,
Source: r.Source(),
}
el := func(gtx Context, idx int) Dimensions {
return Dimensions{Size: image.Pt(10, 10)}
@@ -93,18 +93,18 @@ func TestListPosition(t *testing.T) {
pointer.Event{
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(0, 0),
},
pointer.Event{
Source: pointer.Mouse,
Type: pointer.Scroll,
Kind: pointer.Scroll,
Scroll: f32.Pt(5, 0),
},
pointer.Event{
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(5, 0),
},
)},
@@ -113,18 +113,18 @@ func TestListPosition(t *testing.T) {
pointer.Event{
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(0, 0),
},
pointer.Event{
Source: pointer.Mouse,
Type: pointer.Scroll,
Kind: pointer.Scroll,
Scroll: f32.Pt(3, 0),
},
pointer.Event{
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(5, 0),
},
)},
@@ -133,18 +133,18 @@ func TestListPosition(t *testing.T) {
pointer.Event{
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(0, 0),
},
pointer.Event{
Source: pointer.Mouse,
Type: pointer.Scroll,
Kind: pointer.Scroll,
Scroll: f32.Pt(10, 0),
},
pointer.Event{
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(15, 0),
},
)},
+33
View File
@@ -118,3 +118,36 @@ func (s Stack) Layout(gtx Context, children ...StackChild) Dimensions {
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}
}
+7 -1
View File
@@ -29,7 +29,7 @@ type Op struct {
type Stack struct {
ops *ops.Ops
id ops.StackID
macroID int
macroID uint32
}
var pathSeed maphash.Seed
@@ -204,6 +204,9 @@ func (p *Path) Line(delta f32.Point) {
// LineTo moves the pen to the absolute point specified, recording a line.
func (p *Path) LineTo(to f32.Point) {
if to == p.pen {
return
}
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour))
@@ -250,6 +253,9 @@ func (p *Path) Quad(ctrl, to f32.Point) {
// QuadTo records a quadratic Bézier from the pen to end
// with the control point ctrl, with absolute coordinates.
func (p *Path) QuadTo(ctrl, to f32.Point) {
if ctrl == p.pen && to == p.pen {
return
}
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour))
+5 -17
View File
@@ -53,7 +53,6 @@ The MacroOp records a list of operations to be executed later:
ops := new(op.Ops)
macro := op.Record(ops)
// Record operations by adding them.
op.InvalidateOp{}.Add(ops)
...
// End recording.
call := macro.Stop()
@@ -96,9 +95,9 @@ type CallOp struct {
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.
type InvalidateOp struct {
type InvalidateCmd struct {
At time.Time
}
@@ -111,7 +110,7 @@ type TransformOp struct {
// TransformStack represents a TransformOp pushed on the transformation stack.
type TransformStack struct {
id ops.StackID
macroID int
macroID uint32
ops *ops.Ops
}
@@ -181,19 +180,6 @@ func (c CallOp) Add(o *Ops) {
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.
func Offset(off image.Point) TransformOp {
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[0] = byte(ops.TypePopTransform)
}
func (InvalidateCmd) ImplementsCommand() {}
+49
View File
@@ -15,8 +15,20 @@ import (
"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.
type ImageOp struct {
Filter ImageFilter
uniform bool
color color.NRGBA
src *image.RGBA
@@ -44,6 +56,14 @@ type LinearGradientOp 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 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[0] = byte(ops.TypeImage)
data[1] = byte(i.Filter)
}
func (c ColorOp) Add(o *op.Ops) {
@@ -145,3 +166,31 @@ func Fill(ops *op.Ops, c color.NRGBA) {
ColorOp{Color: c}.Add(ops)
PaintOp{}.Add(ops)
}
// PushOpacity creates a drawing layer with an opacity in the range [0;1].
// The layer includes every subsequent drawing operation until [OpacityStack.Pop]
// is called.
//
// The layer is drawn in two steps. First, the layer operations are
// drawn to a separate image. Then, the image is blended on top of
// the frame, with the opacity used as the blending factor.
func PushOpacity(o *op.Ops, opacity float32) OpacityStack {
if opacity > 1 {
opacity = 1
}
if opacity < 0 {
opacity = 0
}
id, macroID := ops.PushOp(&o.Internal, ops.OpacityStack)
data := ops.Write(&o.Internal, ops.TypePushOpacityLen)
bo := binary.LittleEndian
data[0] = byte(ops.TypePushOpacity)
bo.PutUint32(data[1:], math.Float32bits(opacity))
return OpacityStack{ops: &o.Internal, id: id, macroID: macroID}
}
func (t OpacityStack) Pop() {
ops.PopOp(t.ops, ops.OpacityStack, t.id, t.macroID)
data := ops.Write(t.ops, ops.TypePopOpacityLen)
data[0] = byte(ops.TypePopOpacity)
}
+68 -67
View File
@@ -36,7 +36,8 @@ type document struct {
lines []line
alignment Alignment
// 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
@@ -52,6 +53,7 @@ func (l *document) reset() {
l.lines = l.lines[:0]
l.alignment = Start
l.alignWidth = 0
l.unreadRuneCount = 0
}
func max(a, b int) int {
@@ -93,6 +95,55 @@ type line struct {
yOffset int
}
// insertTrailingSyntheticNewline adds a synthetic newline to the final logical run of the line
// with the given shaping cluster index.
func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) {
// If there was a newline at the end of this paragraph, insert a synthetic glyph representing it.
finalContentRun := len(l.runs) - 1
// If there was a trailing newline update the rune counts to include
// it on the last line of the paragraph.
l.runeCount += 1
l.runs[finalContentRun].Runes.Count += 1
syntheticGlyph := glyph{
id: 0,
clusterIndex: newLineClusterIdx,
glyphCount: 0,
runeCount: 1,
xAdvance: 0,
yAdvance: 0,
xOffset: 0,
yOffset: 0,
}
// Inset the synthetic newline glyph on the proper end of the run.
if l.runs[finalContentRun].Direction.Progression() == system.FromOrigin {
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, syntheticGlyph)
} else {
// Ensure capacity.
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, glyph{})
copy(l.runs[finalContentRun].Glyphs[1:], l.runs[finalContentRun].Glyphs)
l.runs[finalContentRun].Glyphs[0] = syntheticGlyph
}
}
func (l *line) setTruncatedCount(truncatedCount int) {
// If we've truncated the text with a truncator, adjust the rune counts within the
// truncator to make it represent the truncated text.
finalRunIdx := len(l.runs) - 1
l.runs[finalRunIdx].truncator = true
finalGlyphIdx := len(l.runs[finalRunIdx].Glyphs) - 1
// The run represents all of the truncated text.
l.runs[finalRunIdx].Runes.Count = truncatedCount
// Only the final glyph represents any runes, and it represents all truncated text.
for i := range l.runs[finalRunIdx].Glyphs {
if i == finalGlyphIdx {
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncatedCount
} else {
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
}
}
}
// Range describes the position and quantity of a range of text elements
// within a larger slice. The unit is usually runes of unicode data or
// glyphs of shaped font data.
@@ -233,6 +284,7 @@ func (s *shaperImpl) addFace(f font.Face, md giofont.Font) {
if _, ok := s.faceToIndex[f.Font]; ok {
return
}
s.logger.Printf("loaded face %s(style:%s, weight:%d)", md.Typeface, md.Style, md.Weight)
idx := len(s.faces)
s.faceToIndex[f.Font] = idx
s.faces = append(s.faces, f)
@@ -528,35 +580,21 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
if hasNewline {
txt = txt[:len(txt)-1]
}
truncatedNewline := false
if hasNewline && len(txt) == 0 {
params.forceTruncate = false
// If we only have a newline, shape a space to get line metrics.
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))
if params.MaxLines != 0 && hasNewline {
// If we might end up truncating a trailing newline, we must insert the truncator symbol
// on the final line (if we hit the limit).
params.forceTruncate = true
}
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
didTruncate := truncated > 0 || truncatedNewline || (params.forceTruncate && params.MaxLines == len(ls))
if didTruncate && hasNewline {
// We've truncated the newline, since it was at the end and we've truncated some amount of runes
// before it.
hasTruncator := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
if hasTruncator && hasNewline {
// We have a truncator at the end of the line, so the newline is logically
// truncated as well.
truncated++
hasNewline = false
}
// Convert to Lines.
textLines := make([]line, len(ls))
maxHeight := fixed.Int26_6(0)
@@ -565,49 +603,12 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
if otLine.lineHeight > maxHeight {
maxHeight = otLine.lineHeight
}
isFinalLine := i == len(ls)-1
if isFinalLine && hasNewline {
// If there was a trailing newline update the rune counts to include
// 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,
if isFinalLine := i == len(ls)-1; isFinalLine {
if hasNewline {
otLine.insertTrailingSyntheticNewline(len(txt))
}
// Inset the synthetic newline glyph on the proper end of the run.
if otLine.runs[finalRunIdx].Direction.Progression() == system.FromOrigin {
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
}
if hasTruncator {
otLine.setTruncatedCount(truncated)
}
}
textLines[i] = otLine
+19 -24
View File
@@ -3,9 +3,8 @@
package text
import (
"encoding/binary"
"hash/maphash"
"image"
"sync/atomic"
giofont "gioui.org/font"
"gioui.org/io/system"
@@ -88,32 +87,32 @@ type glyphValue[V any] struct {
}
type glyphLRU[V any] struct {
seed maphash.Seed
seed uint64
cache lru[uint64, glyphValue[V]]
}
var seed uint32
// hashGlyphs computes a hash key based on the ID and X offset of
// every glyph in the slice.
func (c *glyphLRU[V]) hashGlyphs(gs []Glyph) uint64 {
if c.seed == (maphash.Seed{}) {
c.seed = maphash.MakeSeed()
if c.seed == 0 {
c.seed = uint64(atomic.AddUint32(&seed, 3900798947))
}
var h maphash.Hash
h.SetSeed(c.seed)
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[:])
if len(gs) == 0 {
return 0
}
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) {
@@ -164,10 +163,6 @@ type layoutKey struct {
lineHeightScale float32
}
type pathKey struct {
gidHash uint64
}
const maxSize = 1000
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
// always be zero. The final glyph in the cluster contains the runes count
// for the entire cluster.
Runes int
Runes uint16
// Flags encode special properties of this glyph.
Flags Flags
}
@@ -245,8 +245,11 @@ func WithCollection(collection []FontFace) ShaperOption {
}
}
// NewShaper constructs a shaper with the provided collection of font faces
// available.
// NewShaper constructs a shaper with the provided options.
//
// NewShaper must be called after [app.NewWindow], unless the [NoSystemFonts]
// option is specified. This is an unfortunate restriction caused by some platforms
// such as Android.
func NewShaper(options ...ShaperOption) *Shaper {
l := &Shaper{}
for _, opt := range options {
@@ -351,11 +354,7 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
unreadRunes++
}
}
lastLineIdx := len(lines.lines) - 1
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.unreadRuneCount = unreadRunes
}
}
l.txt.append(lines)
@@ -470,7 +469,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
Ascent: line.ascent,
Descent: line.descent,
Advance: g.xAdvance,
Runes: g.runeCount,
Runes: uint16(g.runeCount),
Offset: fixed.Point26_6{
X: g.xOffset,
Y: g.yOffset,
@@ -505,6 +504,9 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
}
if endOfCluster {
glyph.Flags |= FlagClusterBreak
if run.truncator {
glyph.Runes += uint16(l.txt.unreadRuneCount)
}
} else {
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() {
glyphs = append(glyphs, g)
if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 {
truncatedRunes += g.Runes
truncatedRunes += int(g.Runes)
} else {
untruncatedRunes += g.Runes
untruncatedRunes += int(g.Runes)
}
if g.Flags&FlagLineBreak != 0 {
lineCount++
@@ -117,9 +117,9 @@ func TestWrappingForcedTruncation(t *testing.T) {
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
glyphs = append(glyphs, g)
if g.Flags&FlagTruncator != 0 && g.Flags&FlagClusterBreak != 0 {
truncatedRunes += g.Runes
truncatedRunes += int(g.Runes)
} else {
untruncatedRunes += g.Runes
untruncatedRunes += int(g.Runes)
}
if g.Flags&FlagLineBreak != 0 {
lineCount++
@@ -154,9 +154,11 @@ func TestWrappingForcedTruncation(t *testing.T) {
// consistently and does not create spurious lines of text.
func TestShapingNewlineHandling(t *testing.T) {
type testcase struct {
textInput string
expectedLines int
expectedGlyphs int
textInput string
expectedLines int
expectedGlyphs int
maxLines int
expectedTruncated int
}
for _, tc := range []testcase{
{textInput: "a\n", expectedLines: 1, expectedGlyphs: 3},
@@ -165,21 +167,41 @@ func TestShapingNewlineHandling(t *testing.T) {
{textInput: "\n", expectedLines: 1, expectedGlyphs: 2},
{textInput: "\n\n", expectedLines: 2, expectedGlyphs: 3},
{textInput: "\n\n\n", expectedLines: 3, expectedGlyphs: 4},
{textInput: "\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 1},
{textInput: "\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 2},
{textInput: "\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 3},
{textInput: "a\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 1},
{textInput: "a\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 2},
{textInput: "a\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 3},
{textInput: "\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 2},
{textInput: "\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 1},
{textInput: "\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 2},
{textInput: "a\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 3},
{textInput: "a\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 1},
{textInput: "a\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 2},
} {
t.Run(fmt.Sprintf("%q", 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)
collection := []FontFace{{Face: ltrFace}}
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
checkGlyphs := func() {
glyphs := []Glyph{}
runes := 0
truncated := 0
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
glyphs = append(glyphs, g)
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)
}
if truncated != tc.expectedTruncated {
t.Errorf("expected %d truncated runes, got %d", tc.expectedTruncated, truncated)
}
if len(glyphs) != tc.expectedGlyphs {
t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs))
}
@@ -207,29 +229,27 @@ func TestShapingNewlineHandling(t *testing.T) {
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)
} 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,
PxPerEm: fixed.I(10),
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, tc.textInput)
MaxLines: tc.maxLines,
}
cache.LayoutString(params, tc.textInput)
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount)
}
checkGlyphs()
cache.Layout(Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, strings.NewReader(tc.textInput))
cache.Layout(params, strings.NewReader(tc.textInput))
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount)
}
@@ -551,7 +571,7 @@ func TestShapeStringRuneAccounting(t *testing.T) {
}
totalRunes := 0
for _, g := range glyphs {
totalRunes += g.Runes
totalRunes += int(g.Runes)
}
if inputRunes := len([]rune(tc.input)); totalRunes != inputRunes {
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
clk Clickable
changed bool
}
// Changed reports whether Value has changed since the last
// call to Changed.
func (b *Bool) Changed() bool {
changed := b.changed
b.changed = false
// Update the widget state and report whether Value was changed.
func (b *Bool) Update(gtx layout.Context) bool {
changed := false
for b.clk.clicked(b, gtx) {
b.Value = !b.Value
changed = true
}
return changed
}
@@ -33,23 +33,15 @@ func (b *Bool) Pressed() bool {
return b.clk.Pressed()
}
// Focused reports whether b has focus.
func (b *Bool) Focused() bool {
return b.clk.Focused()
}
func (b *Bool) History() []Press {
return b.clk.History()
}
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
for b.clk.Clicked() {
b.Value = !b.Value
b.changed = true
}
b.Update(gtx)
dims := b.clk.layout(b, gtx, func(gtx layout.Context) layout.Dimensions {
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 dims
-3
View File
@@ -11,9 +11,6 @@ import (
// editBuffer implements a gap buffer for text editing.
type editBuffer struct {
// pos is the byte position for Read and ReadRune.
pos int
// The gap start and end in bytes.
gapstart, gapend int
text []byte
+86 -89
View File
@@ -7,6 +7,7 @@ import (
"time"
"gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
@@ -17,17 +18,11 @@ import (
// Clickable represents a clickable area.
type Clickable struct {
click gesture.Click
clicks []Click
// prevClicks is the index into clicks that marks the clicks
// from the most recent Layout call. prevClicks is used to keep
// clicks bounded.
prevClicks int
history []Press
click gesture.Click
history []Press
keyTag struct{}
requestFocus bool
focused bool
requestClicks int
pressedKey key.Name
}
// Click represents a click.
@@ -49,26 +44,19 @@ type Press struct {
Cancelled bool
}
// Click executes a simple programmatic click
// Click executes a simple programmatic click.
func (b *Clickable) Click() {
b.clicks = append(b.clicks, Click{
Modifiers: 0,
NumClicks: 1,
})
b.requestClicks++
}
// Clicked reports whether there are pending clicks as would be
// reported by Clicks. If so, Clicked removes the earliest click.
func (b *Clickable) Clicked() bool {
if len(b.clicks) == 0 {
return false
}
n := copy(b.clicks, b.clicks[1:])
b.clicks = b.clicks[:n]
if b.prevClicks > 0 {
b.prevClicks--
}
return true
// Clicked calls Update and reports whether a click was registered.
func (b *Clickable) Clicked(gtx layout.Context) bool {
return b.clicked(b, gtx)
}
func (b *Clickable) clicked(t event.Tag, gtx layout.Context) bool {
_, clicked := b.update(t, gtx)
return clicked
}
// Hovered reports whether a pointer is over the element.
@@ -81,54 +69,42 @@ func (b *Clickable) Pressed() bool {
return b.click.Pressed()
}
// Focus requests the input focus for the element.
func (b *Clickable) Focus() {
b.requestFocus = true
}
// Focused reports whether b has focus.
func (b *Clickable) Focused() bool {
return b.focused
}
// Clicks returns and clear the clicks since the last call to Clicks.
func (b *Clickable) Clicks() []Click {
clicks := b.clicks
b.clicks = nil
b.prevClicks = 0
return clicks
}
// History is the past pointer presses useful for drawing markers.
// History is retained for a short duration (about a second).
func (b *Clickable) History() []Press {
return b.history
}
// Layout and update the button state
// Layout and update the button state.
func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
b.update(gtx)
return b.layout(b, gtx, w)
}
func (b *Clickable) layout(t event.Tag, gtx layout.Context, w layout.Widget) layout.Dimensions {
for {
_, ok := b.update(t, gtx)
if !ok {
break
}
}
m := op.Record(gtx.Ops)
dims := w(gtx)
c := m.Stop()
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
disabled := gtx.Queue == nil
semantic.DisabledOp(disabled).Add(gtx.Ops)
semantic.EnabledOp(gtx.Enabled()).Add(gtx.Ops)
b.click.Add(gtx.Ops)
if !disabled {
keys := key.Set("⏎|Space")
if !b.focused {
keys = ""
}
key.InputOp{Tag: &b.keyTag, Keys: keys}.Add(gtx.Ops)
if b.requestFocus {
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
b.requestFocus = false
}
} else {
b.focused = false
}
event.Op(gtx.Ops, t)
c.Add(gtx.Ops)
return dims
}
// Update the button state by processing events, and return the next
// click, if any.
func (b *Clickable) Update(gtx layout.Context) (Click, bool) {
return b.update(b, gtx)
}
func (b *Clickable) update(t event.Tag, gtx layout.Context) (Click, bool) {
for len(b.history) > 0 {
c := b.history[0]
if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
@@ -137,36 +113,36 @@ func (b *Clickable) Layout(gtx layout.Context, w layout.Widget) layout.Dimension
n := copy(b.history, b.history[1:])
b.history = b.history[:n]
}
return dims
}
// update the button state by processing events.
func (b *Clickable) update(gtx layout.Context) {
// Flush clicks from before the last update.
n := copy(b.clicks, b.clicks[b.prevClicks:])
b.clicks = b.clicks[:n]
b.prevClicks = n
for _, e := range b.click.Events(gtx) {
switch e.Type {
case gesture.TypeClick:
b.clicks = append(b.clicks, Click{
Modifiers: e.Modifiers,
NumClicks: e.NumClicks,
})
if c := b.requestClicks; c > 0 {
b.requestClicks = 0
return Click{
NumClicks: c,
}, true
}
for {
e, ok := b.click.Update(gtx.Source)
if !ok {
break
}
switch e.Kind {
case gesture.KindClick:
if l := len(b.history); l > 0 {
b.history[l-1].End = gtx.Now
}
case gesture.TypeCancel:
return Click{
Modifiers: e.Modifiers,
NumClicks: e.NumClicks,
}, true
case gesture.KindCancel:
for i := range b.history {
b.history[i].Cancelled = true
if b.history[i].End.IsZero() {
b.history[i].End = gtx.Now
}
}
case gesture.TypePress:
case gesture.KindPress:
if e.Source == pointer.Mouse {
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
gtx.Execute(key.FocusCmd{Tag: t})
}
b.history = append(b.history, Press{
Position: e.Position,
@@ -174,21 +150,42 @@ func (b *Clickable) update(gtx layout.Context) {
})
}
}
for _, e := range gtx.Events(&b.keyTag) {
for {
e, ok := gtx.Event(
key.FocusFilter{Target: t},
key.Filter{Focus: t, Name: key.NameReturn},
key.Filter{Focus: t, Name: key.NameSpace},
)
if !ok {
break
}
switch e := e.(type) {
case key.FocusEvent:
b.focused = e.Focus
if e.Focus {
b.pressedKey = ""
}
case key.Event:
if !b.focused || e.State != key.Release {
if !gtx.Focused(t) {
break
}
if e.Name != key.NameReturn && e.Name != key.NameSpace {
break
}
b.clicks = append(b.clicks, Click{
Modifiers: e.Modifiers,
NumClicks: 1,
})
switch e.State {
case key.Press:
b.pressedKey = e.Name
case key.Release:
if b.pressedKey != e.Name {
break
}
// only register a key as a click if the key was pressed and released while this button was focused
b.pressedKey = ""
return Click{
Modifiers: e.Modifiers,
NumClicks: 1,
}, true
}
}
}
return Click{}, false
}
+94
View File
@@ -0,0 +1,94 @@
// SPDX-License-Identifier: Unlicense OR MIT
package widget_test
import (
"image"
"testing"
"gioui.org/io/input"
"gioui.org/io/key"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget"
)
func TestClickable(t *testing.T) {
var (
r input.Router
b1 widget.Clickable
b2 widget.Clickable
)
gtx := layout.Context{
Ops: new(op.Ops),
Source: r.Source(),
}
layout := func() {
b1.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{Size: image.Pt(100, 100)}
})
// buttons are on top of each other but we only use focus and keyevents, so this is fine
b2.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Dimensions{Size: image.Pt(100, 100)}
})
}
frame := func() {
gtx.Reset()
layout()
r.Frame(gtx.Ops)
}
gtx.Execute(key.FocusCmd{Tag: &b1})
frame()
if !gtx.Focused(&b1) {
t.Error("button 1 did not gain focus")
}
if gtx.Focused(&b2) {
t.Error("button 2 should not have focus")
}
r.Queue(
key.Event{
Name: key.NameReturn,
State: key.Press,
},
key.Event{
Name: key.NameReturn,
State: key.Release,
},
)
if !b1.Clicked(gtx) {
t.Error("button 1 did not get clicked when it got return press & release")
}
if b2.Clicked(gtx) {
t.Error("button 2 got clicked when it did not have focus")
}
r.Queue(
key.Event{
Name: key.NameReturn,
State: key.Press,
},
)
if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it only got return press")
}
frame()
gtx.Execute(key.FocusCmd{Tag: &b2})
frame()
if gtx.Focused(&b1) {
t.Error("button 1 should not have focus")
}
if !gtx.Focused(&b2) {
t.Error("button 2 did not gain focus")
}
r.Queue(
key.Event{
Name: key.NameReturn,
State: key.Release,
},
)
if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it had lost focus")
}
if b2.Clicked(gtx) {
t.Error("button 2 should not have been clicked, as it only got return release")
}
}
+28 -27
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"math/bits"
"gioui.org/gesture"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op/clip"
@@ -12,12 +11,7 @@ import (
// Decorations handles the states of window decorations.
type Decorations struct {
clicks []Clickable
resize [8]struct {
gesture.Hover
gesture.Drag
}
actions system.Action
clicks map[int]*Clickable
maximized bool
}
@@ -35,22 +29,13 @@ func (d *Decorations) Clickable(action system.Action) *Clickable {
panic(fmt.Errorf("not a single action"))
}
idx := bits.TrailingZeros(uint(action))
if n := idx - len(d.clicks); n >= 0 {
d.clicks = append(d.clicks, make([]Clickable, n+1)...)
}
click := &d.clicks[idx]
if click.Clicked() {
if action == system.ActionMaximize {
if d.maximized {
d.maximized = false
d.actions |= system.ActionUnmaximize
} else {
d.maximized = true
d.actions |= system.ActionMaximize
}
} else {
d.actions |= action
click, found := d.clicks[idx]
if !found {
click = new(Clickable)
if d.clicks == nil {
d.clicks = make(map[int]*Clickable)
}
d.clicks[idx] = click
}
return click
}
@@ -66,11 +51,27 @@ func (d *Decorations) Perform(actions system.Action) {
}
}
// Actions returns the set of actions activated by the user.
func (d *Decorations) Actions() system.Action {
a := d.actions
d.actions = 0
return a
// Update the state and return the set of actions activated by the user.
func (d *Decorations) Update(gtx layout.Context) system.Action {
var actions system.Action
for idx, clk := range d.clicks {
if !clk.Clicked(gtx) {
continue
}
action := system.Action(1 << idx)
switch {
case action == system.ActionMaximize && d.maximized:
action = system.ActionUnmaximize
case action == system.ActionUnmaximize && !d.maximized:
action = system.ActionMaximize
}
switch action {
case system.ActionMaximize, system.ActionUnmaximize:
d.maximized = !d.maximized
}
actions |= action
}
return actions
}
// Maximized returns whether the window is maximized.
+38 -48
View File
@@ -5,6 +5,7 @@ import (
"gioui.org/f32"
"gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/pointer"
"gioui.org/io/transfer"
"gioui.org/layout"
@@ -17,54 +18,25 @@ type Draggable struct {
// Type contains the MIME type and matches transfer.SourceOp.
Type string
handle struct{}
drag gesture.Drag
click f32.Point
pos f32.Point
requested bool
request string
drag gesture.Drag
click f32.Point
pos f32.Point
}
func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dimensions {
if gtx.Queue == nil {
if !gtx.Enabled() {
return w(gtx)
}
pos := d.pos
for _, ev := range d.drag.Events(gtx.Metric, gtx.Queue, gesture.Both) {
switch ev.Type {
case pointer.Press:
d.click = ev.Position
pos = f32.Point{}
case pointer.Drag, pointer.Release:
pos = ev.Position.Sub(d.click)
}
}
d.pos = pos
for _, ev := range gtx.Queue.Events(&d.handle) {
switch e := ev.(type) {
case transfer.RequestEvent:
d.requested = true
d.request = e.Type
case transfer.CancelEvent:
d.requested = false
d.request = ""
}
}
dims := w(gtx)
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
d.drag.Add(gtx.Ops)
transfer.SourceOp{
Tag: &d.handle,
Type: d.Type,
}.Add(gtx.Ops)
event.Op(gtx.Ops, d)
stack.Pop()
if drag != nil && d.drag.Pressed() {
rec := op.Record(gtx.Ops)
op.Offset(pos.Round()).Add(gtx.Ops)
op.Offset(d.pos.Round()).Add(gtx.Ops)
drag(gtx)
op.Defer(gtx.Ops, rec.Stop())
}
@@ -77,23 +49,41 @@ func (d *Draggable) Dragging() bool {
return d.drag.Dragging()
}
// Requested returns the MIME type, if any, for which the Draggable was requested to offer data.
func (d *Draggable) Requested() (mime string, requested bool) {
mime = d.request
requested = d.requested
d.requested = false
d.request = ""
return
// Update the draggable and returns the MIME type for which the Draggable was
// requested to offer data, if any
func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
pos := d.pos
for {
ev, ok := d.drag.Update(gtx.Metric, gtx.Source, gesture.Both)
if !ok {
break
}
switch ev.Kind {
case pointer.Press:
d.click = ev.Position
pos = f32.Point{}
case pointer.Drag, pointer.Release:
pos = ev.Position.Sub(d.click)
}
}
d.pos = pos
for {
e, ok := gtx.Event(transfer.SourceFilter{Target: d, Type: d.Type})
if !ok {
break
}
if e, ok := e.(transfer.RequestEvent); ok {
return e.Type, true
}
}
return "", false
}
// Offer the data ready for a drop. Must be called after being Requested.
// The mime must be one in the requested list.
func (d *Draggable) Offer(ops *op.Ops, mime string, data io.ReadCloser) {
transfer.OfferOp{
Tag: &d.handle,
Type: mime,
Data: data,
}.Add(ops)
func (d *Draggable) Offer(gtx layout.Context, mime string, data io.ReadCloser) {
gtx.Execute(transfer.OfferCmd{Tag: d, Type: mime, Data: data})
}
// Pos returns the drag position relative to its initial click position.

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