Compare commits

..

39 Commits

Author SHA1 Message Date
Chris Waldon 718be79d9e 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-08 08:18:09 -05:00
Dominik Honnef 0073e1a167 widget: don't refer to non-existent method Clickable.Clicks
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2024-01-08 08:18:09 -05:00
Dominik Honnef 32ecec5538 gesture: adjust ClickKind.String for ClickType -> ClickKind rename
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2024-01-08 08:18:09 -05:00
Elias Naur 6eb33b8a56 app: [Windows] tolerate gpu.ErrDeviceLost from Refresh
Fixes: https://todo.sr.ht/~eliasnaur/gio/552
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-01-08 08:18:09 -05:00
Elias Naur 4617526e12 app: don't route internal wakeup events to the Router
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-01-08 08:18:09 -05:00
Elias Naur dbc7a900bd app: [Windows] fix restore size when leaving fullscreen
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-01-08 08:18:09 -05:00
Chris Waldon fb3ae95b28 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>
2024-01-08 08:18:09 -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
66 changed files with 996 additions and 856 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
+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
+6 -5
View File
@@ -12,16 +12,16 @@ 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 receiving events from its Events channel. The most
important event is FrameEvent that prompts an update of the window
contents and state.
A Window is run by calling NextEvent in a loop. 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() {
for {
e := w.NextEvent()
if e, ok := e.(system.FrameEvent); ok {
ops.Reset()
// Add operations to ops.
@@ -50,7 +50,8 @@ For example, to display a blank but otherwise functional window:
func main() {
go func() {
w := app.NewWindow()
for range w.Events() {
for {
w.NextEvent()
}
}()
app.Main()
+7 -7
View File
@@ -953,18 +953,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
}
@@ -994,7 +994,7 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
return
}
w.callbacks.Event(pointer.Event{
Type: typ,
Kind: kind,
Source: src,
Buttons: btns,
PointerID: pointer.ID(pointerID),
+1 -1
View File
@@ -124,7 +124,7 @@ func stringToNSString(str string) C.CFTypeRef {
return C.newNSString(chars, C.NSUInteger(len(u16)))
}
func NewDisplayLink(callback func()) (*displayLink, error) {
func newDisplayLink(callback func()) (*displayLink, error) {
d := &displayLink{
callback: callback,
done: make(chan struct{}),
+7 -7
View File
@@ -117,7 +117,7 @@ func onCreate(view, controller C.CFTypeRef) {
w := &window{
view: view,
}
dl, err := NewDisplayLink(func() {
dl, err := newDisplayLink(func() {
w.draw(false)
})
if err != nil {
@@ -236,16 +236,16 @@ func onText(view, str C.CFTypeRef) {
//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
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
}
@@ -253,7 +253,7 @@ func onTouch(last C.int, view, touchRef C.CFTypeRef, phase C.NSInteger, x, y C.C
t := time.Duration(float64(ti) * float64(time.Second))
p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{
Type: typ,
Kind: kind,
Source: pointer.Touch,
PointerID: w.lookupTouch(last != 0, touchRef),
Position: p,
+5 -5
View File
@@ -275,7 +275,7 @@ func (w *window) addEventListeners() {
}
w.touches = w.touches[:0]
w.w.Event(pointer.Event{
Type: pointer.Cancel,
Kind: pointer.Cancel,
Source: pointer.Touch,
})
return nil
@@ -398,7 +398,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")
@@ -426,7 +426,7 @@ func (w *window) touchEvent(typ pointer.Type, e js.Value) {
Y: float32(y) * scale,
}
w.w.Event(pointer.Event{
Type: typ,
Kind: kind,
Source: pointer.Touch,
Position: pos,
PointerID: pid,
@@ -448,7 +448,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")
@@ -476,7 +476,7 @@ func (w *window) pointerEvent(typ pointer.Type, dx, dy float32, e js.Value) {
btns |= pointer.ButtonTertiary
}
w.w.Event(pointer.Event{
Type: typ,
Kind: kind,
Source: pointer.Mouse,
Buttons: btns,
Position: pos,
+10 -4
View File
@@ -192,6 +192,10 @@ static CFTypeRef windowForView(CFTypeRef viewRef) {
}
static void raiseWindow(CFTypeRef windowRef) {
NSRunningApplication *currentApp = [NSRunningApplication currentApplication];
if (![currentApp isActive]) {
[currentApp activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
}
NSWindow* window = (__bridge NSWindow *)windowRef;
[window makeKeyAndOrderFront:nil];
}
@@ -297,7 +301,9 @@ 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})
}
@@ -526,7 +532,7 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
case 2:
btn = pointer.ButtonTertiary
}
var typ pointer.Type
var typ pointer.Kind
switch cdir {
case C.MOUSE_MOVE:
typ = pointer.Move
@@ -550,7 +556,7 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
panic("invalid direction")
}
w.w.Event(pointer.Event{
Type: typ,
Kind: typ,
Source: pointer.Mouse,
Time: t,
Buttons: w.pointerBtns,
@@ -884,7 +890,7 @@ func newOSWindow() (*window, error) {
scale: scale,
redraw: make(chan struct{}, 1),
}
dl, err := NewDisplayLink(func() {
dl, err := newDisplayLink(func() {
select {
case w.redraw <- struct{}{}:
default:
+12 -22
View File
@@ -94,10 +94,7 @@ type wlDisplay struct {
// Notification pipe fds.
notify struct {
read int
mu sync.Mutex
write int
read, write int
}
repeat repeatState
@@ -794,7 +791,7 @@ func gio_onTouchDown(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
@@ -810,7 +807,7 @@ func gio_onTouchUp(data unsafe.Pointer, touch *C.struct_wl_touch, serial, t C.ui
w := s.touchFoci[id]
delete(s.touchFoci, id)
w.w.Event(pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Source: pointer.Touch,
Position: w.lastTouch,
PointerID: pointer.ID(id),
@@ -828,7 +825,7 @@ func gio_onTouchMotion(data unsafe.Pointer, touch *C.struct_wl_touch, t C.uint32
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: w.lastTouch,
Source: pointer.Touch,
PointerID: pointer.ID(id),
@@ -847,7 +844,7 @@ func gio_onTouchCancel(data unsafe.Pointer, touch *C.struct_wl_touch) {
for id, w := range s.touchFoci {
delete(s.touchFoci, id)
w.w.Event(pointer.Event{
Type: pointer.Cancel,
Kind: pointer.Cancel,
Source: pointer.Touch,
})
}
@@ -872,7 +869,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.w.Event(pointer.Event{Kind: pointer.Cancel})
}
}
@@ -920,21 +917,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,
Kind: kind,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: w.lastPos,
@@ -1445,11 +1442,6 @@ func (w *window) SetAnimating(anim bool) {
// Wakeup wakes up the event loop through the notification pipe.
func (d *wlDisplay) wakeup() {
oneByte := make([]byte, 1)
d.notify.mu.Lock()
defer d.notify.mu.Unlock()
if d.notify.write == 0 {
return
}
if _, err := syscall.Write(d.notify.write, oneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
@@ -1581,7 +1573,7 @@ func (w *window) flushScroll() {
return
}
w.w.Event(pointer.Event{
Type: pointer.Scroll,
Kind: pointer.Scroll,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: w.lastPos,
@@ -1604,7 +1596,7 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
Y: fromFixed(y) * float32(w.scale),
}
w.w.Event(pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: w.lastPos,
Buttons: w.pointerBtns,
Source: pointer.Mouse,
@@ -1828,12 +1820,10 @@ func newWLDisplay() (*wlDisplay, error) {
}
func (d *wlDisplay) destroy() {
d.notify.mu.Lock()
if d.notify.write != 0 {
syscall.Close(d.notify.write)
d.notify.write = 0
}
d.notify.mu.Unlock()
if d.notify.read != 0 {
syscall.Close(d.notify.read)
d.notify.read = 0
+9 -8
View File
@@ -259,7 +259,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
w.pointerButton(pointer.ButtonTertiary, false, lParam, getModifiers())
case windows.WM_CANCELMODE:
w.w.Event(pointer.Event{
Type: pointer.Cancel,
Kind: pointer.Cancel,
})
case windows.WM_SETFOCUS:
w.focused = true
@@ -288,7 +288,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
x, y := coordsFromlParam(lParam)
p := f32.Point{X: float32(x), Y: float32(y)}
w.w.Event(pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Source: pointer.Mouse,
Position: p,
Buttons: w.pointerBtns,
@@ -501,15 +501,15 @@ func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr,
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()
@@ -518,7 +518,7 @@ 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,
Kind: kind,
Source: pointer.Mouse,
Position: p,
Buttons: w.pointerBtns,
@@ -553,7 +553,7 @@ func (w *window) scrollEvent(wParam, lParam uintptr, horizontal bool, kmods key.
}
}
w.w.Event(pointer.Event{
Type: pointer.Scroll,
Kind: pointer.Scroll,
Source: pointer.Mouse,
Position: p,
Buttons: w.pointerBtns,
@@ -721,11 +721,12 @@ func (w *window) Configure(options []Option) {
}
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)
+7 -7
View File
@@ -547,7 +547,7 @@ func (h *x11EventHandler) handleEvents() bool {
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 +557,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 +569,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 +578,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 +587,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
@@ -607,7 +607,7 @@ func (h *x11EventHandler) handleEvents() bool {
case C.MotionNotify:
mevt := (*C.XMotionEvent)(unsafe.Pointer(xev))
w.w.Event(pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Source: pointer.Mouse,
Buttons: w.pointerBtns,
Position: f32.Point{
+56 -31
View File
@@ -61,6 +61,8 @@ type Window struct {
// actions are the actions waiting to be performed.
actions chan system.Action
// out is where the platform backend delivers events bound for the
// user program.
out chan event.Event
frames chan *op.Ops
frameAck chan struct{}
@@ -107,6 +109,16 @@ type Window struct {
}
imeState editorState
// event stores the state required for processing and delivering events
// from NextEvent. If we had support for range over func, this would
// be the iterator state.
eventState struct {
created bool
initialOpts []Option
wakeup func()
timer *time.Timer
}
}
type editorState struct {
@@ -185,7 +197,7 @@ func NewWindow(options ...Option) *Window {
w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[router.SemanticID]router.SemanticNode)
w.callbacks.w = w
go w.run(options)
w.eventState.initialOpts = options
return w
}
@@ -195,11 +207,6 @@ func decoHeightOpt(h unit.Dp) Option {
}
}
// Events returns the channel where events are delivered.
func (w *Window) Events() <-chan event.Event {
return w.out
}
// update the window contents, input operations declare input handlers,
// and so on. The supplied operations list completely replaces the window state
// from previous calls.
@@ -713,7 +720,7 @@ func (w *Window) waitAck(d driver) {
select {
case f := <-w.driverFuncs:
f(d)
case w.out <- event.Event(nil):
case w.out <- theFlushEvent:
// A dummy event went through, so we know the application has processed the previous event.
return
case <-w.immediateRedraws:
@@ -747,7 +754,7 @@ func (w *Window) waitFrame(d driver) *op.Ops {
case frame := <-w.frames:
// The client called FrameEvent.Frame.
return frame
case w.out <- event.Event(nil):
case w.out <- theFlushEvent:
// The client ignored FrameEvent and continued processing
// events.
return nil
@@ -881,7 +888,6 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
if err := w.validateAndProcess(d, viewSize, e2.Sync, wrapper, signal); err != nil {
w.destroyGPU()
w.out <- system.DestroyEvent{Err: err}
close(w.out)
close(w.destroy)
break
}
@@ -890,7 +896,6 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
case system.DestroyEvent:
w.destroyGPU()
w.out <- e2
close(w.out)
close(w.destroy)
case ViewEvent:
w.out <- e2
@@ -899,6 +904,7 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
w.decorations.Config = e2.Config
e2.Config = w.effectiveConfig()
w.out <- e2
case wakeupEvent:
case event.Event:
handled := w.queue.q.Queue(e2)
if e, ok := e.(key.Event); ok && !handled {
@@ -938,43 +944,51 @@ func (w *Window) processEvent(d driver, e event.Event) bool {
return true
}
func (w *Window) run(options []Option) {
if err := newWindow(&w.callbacks, options); err != nil {
w.out <- system.DestroyEvent{Err: err}
close(w.out)
close(w.destroy)
return
// NextEvent blocks until an event is received from the window, such as
// [io/system.FrameEvent]. It blocks forever if called after [io/system.DestroyEvent]
// has been returned.
func (w *Window) NextEvent() event.Event {
state := &w.eventState
if !state.created {
state.created = true
if err := newWindow(&w.callbacks, state.initialOpts); err != nil {
close(w.destroy)
return system.DestroyEvent{Err: err}
}
}
var wakeup func()
var timer *time.Timer
for {
var (
wakeups <-chan struct{}
timeC <-chan time.Time
)
if wakeup != nil {
if state.wakeup != nil {
wakeups = w.wakeups
if timer != nil {
timeC = timer.C
if state.timer != nil {
timeC = state.timer.C
}
}
select {
case t := <-w.scheduledRedraws:
if timer != nil {
timer.Stop()
if state.timer != nil {
state.timer.Stop()
}
timer = time.NewTimer(time.Until(t))
case <-w.destroy:
return
state.timer = time.NewTimer(time.Until(t))
case e := <-w.out:
// Receiving a flushEvent indicates to the platform backend that
// all previous events have been processed by the user program.
if _, ok := e.(flushEvent); ok {
break
}
return e
case <-timeC:
select {
case w.redraws <- struct{}{}:
wakeup()
state.wakeup()
default:
}
case <-wakeups:
wakeup()
case wakeup = <-w.wakeupFuncs:
state.wakeup()
case state.wakeup = <-w.wakeupFuncs:
}
}
}
@@ -1022,9 +1036,9 @@ func (w *Window) decorate(d driver, e system.FrameEvent, o *op.Ops) (size, offse
Metric: e.Metric,
Constraints: layout.Exact(e.Size),
}
style.Layout(gtx)
// Update the window based on the actions on the decorations.
w.Perform(deco.Actions())
w.Perform(deco.Update(gtx))
style.Layout(gtx)
// Offset to place the frame content below the decorations.
decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
if w.decorations.currentHeight != decoHeight {
@@ -1165,3 +1179,14 @@ func Decorated(enabled bool) Option {
cnf.Decorated = enabled
}
}
// flushEvent is sent to detect when the user program
// has completed processing of all prior events. Its an
// [io/event.Event] but only for internal use.
type flushEvent struct{}
func (t flushEvent) ImplementsEvent() {}
// theFlushEvent avoids allocating garbage when sending
// flushEvents.
var theFlushEvent flushEvent
+38 -39
View File
@@ -39,18 +39,18 @@ type Hover struct {
func (h *Hover) Add(ops *op.Ops) {
pointer.InputOp{
Tag: h,
Types: pointer.Enter | pointer.Leave,
Kinds: pointer.Enter | pointer.Leave,
}.Add(ops)
}
// Hovered returns whether a pointer is inside the area.
func (h *Hover) Hovered(q event.Queue) bool {
// Update state and report whether a pointer is inside the area.
func (h *Hover) Update(q event.Queue) bool {
for _, ev := range q.Events(h) {
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 +87,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 +99,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 {
@@ -136,15 +136,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 (
@@ -163,7 +163,7 @@ const touchSlop = unit.Dp(3)
func (c *Click) Add(ops *op.Ops) {
pointer.InputOp{
Tag: c,
Types: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
}.Add(ops)
}
@@ -177,24 +177,24 @@ func (c *Click) Pressed() bool {
return c.pressed
}
// Events returns the next click events, if any.
func (c *Click) Events(q event.Queue) []ClickEvent {
// Update state and return the click events.
func (c *Click) Update(q event.Queue) []ClickEvent {
var events []ClickEvent
for _, evt := range q.Events(c) {
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})
events = append(events, ClickEvent{Kind: KindClick, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
} else {
events = append(events, ClickEvent{Type: TypeCancel})
events = append(events, ClickEvent{Kind: KindCancel})
}
case pointer.Cancel:
wasPressed := c.pressed
@@ -202,7 +202,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
c.hovered = false
c.entered = false
if wasPressed {
events = append(events, ClickEvent{Type: TypeCancel})
events = append(events, ClickEvent{Kind: KindCancel})
}
case pointer.Press:
if c.pressed {
@@ -224,7 +224,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})
events = append(events, ClickEvent{Kind: KindPress, Position: e.Position.Round(), Source: e.Source, Modifiers: e.Modifiers, NumClicks: c.clicks})
case pointer.Leave:
if !c.pressed {
c.pid = e.PointerID
@@ -254,7 +254,7 @@ 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,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
ScrollBounds: bounds,
}
oph.Add(ops)
@@ -268,9 +268,8 @@ 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 {
// Update state and report the scroll distance along axis.
func (s *Scroll) Update(cfg unit.Metric, q event.Queue, t time.Time, axis Axis) int {
if s.axis != axis {
s.axis = axis
return 0
@@ -281,7 +280,7 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
if !ok {
continue
}
switch e.Type {
switch e.Kind {
case pointer.Press:
if s.dragging {
break
@@ -368,12 +367,12 @@ func (d *Drag) Add(ops *op.Ops) {
pointer.InputOp{
Tag: d,
Grab: d.grab,
Types: pointer.Press | pointer.Drag | pointer.Release,
Kinds: pointer.Press | pointer.Drag | pointer.Release,
}.Add(ops)
}
// Events returns the next drag events, if any.
func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event {
// Update state and return the drag events.
func (d *Drag) Update(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)
@@ -381,7 +380,7 @@ func (d *Drag) Events(cfg unit.Metric, q event.Queue, axis Axis) []pointer.Event
continue
}
switch e.Type {
switch e.Kind {
case pointer.Press:
if !(e.Buttons == pointer.ButtonPrimary || e.Source == pointer.Touch) {
continue
@@ -444,16 +443,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")
}
}
+8 -8
View File
@@ -26,16 +26,16 @@ func TestHover(t *testing.T) {
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) {
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) {
t.Fatal("expected not hovered")
}
}
@@ -75,7 +75,7 @@ func TestMouseClicks(t *testing.T) {
r.Frame(&ops)
r.Queue(tc.events...)
events := click.Events(&r)
events := click.Update(&r)
clicks := filterMouseClicks(events)
if got, want := len(clicks), len(tc.clicks); got != want {
t.Fatalf("got %d mouse clicks, expected %d", got, want)
@@ -92,7 +92,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,7 +101,7 @@ 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
@@ -110,7 +110,7 @@ func mouseClickEvents(times ...time.Duration) []event.Event {
func filterMouseClicks(events []ClickEvent) []ClickEvent {
var clicks []ClickEvent
for _, ev := range events {
if ev.Type == TypeClick {
if ev.Kind == KindClick {
clicks = append(clicks, ev)
}
}
+32 -3
View File
@@ -186,10 +186,16 @@ type material struct {
uvTrans f32.Affine2D
}
const (
filterLinear = 0
filterNearest = 1
)
// imageOpData is the shadow of paint.ImageOp.
type imageOpData struct {
src *image.RGBA
handle interface{}
filter byte
}
type linearGradientOpData struct {
@@ -207,6 +213,7 @@ func decodeImageOp(data []byte, refs []interface{}) imageOpData {
return imageOpData{
src: refs[0].(*image.RGBA),
handle: handle,
filter: data[1],
}
}
@@ -454,19 +461,41 @@ func (g *gpu) Profile() string {
}
func (r *renderer) texHandle(cache *resourceCache, data imageOpData) driver.Texture {
type cachekey struct {
filter byte
handle any
}
key := cachekey{
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)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

+67
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
+21 -21
View File
@@ -14,7 +14,7 @@ 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.
@@ -32,7 +32,7 @@ type Ops struct {
stringRefs []string
// nextStateID is the id allocated for the next
// StateOp.
nextStateID int
nextStateID uint32
// multipOp indicates a multi-op such as clip.Path is being added.
multipOp bool
@@ -84,30 +84,30 @@ const (
TypeSemanticDesc
TypeSemanticClass
TypeSemanticSelected
TypeSemanticDisabled
TypeSemanticEnabled
TypeSnippet
TypeSelection
TypeActionInput
)
type StackID struct {
id int
prev int
id uint32
prev uint32
}
// StateOp represents a saved operation snapshot to be restored
// later.
type StateOp struct {
id int
macroID int
id uint32
macroID uint32
ops *Ops
}
// stack tracks the integer identities of stack operations to ensure correct
// pairing of their push and pop methods.
type stack struct {
currentID int
nextID int
currentID uint32
nextID uint32
}
type StackKind uint8
@@ -142,7 +142,7 @@ const (
TypePushOpacityLen = 1 + 4
TypePopOpacityLen = 1
TypeRedrawLen = 1 + 8
TypeImageLen = 1
TypeImageLen = 1 + 1
TypePaintLen = 1
TypeColorLen = 1 + 4
TypeLinearGradientLen = 1 + 8*2 + 4*2
@@ -170,7 +170,7 @@ const (
TypeSemanticDescLen = 1
TypeSemanticClassLen = 2
TypeSemanticSelectedLen = 2
TypeSemanticDisabledLen = 2
TypeSemanticEnabledLen = 2
TypeSnippetLen = 1 + 4 + 4
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
TypeActionInputLen = 1 + 1
@@ -266,11 +266,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")
}
@@ -310,7 +310,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 {
@@ -454,23 +454,23 @@ var opProps = [0x100]opProp{
TypeSemanticDesc: {Size: TypeSemanticDescLen, NumRefs: 1},
TypeSemanticClass: {Size: TypeSemanticClassLen, NumRefs: 0},
TypeSemanticSelected: {Size: TypeSemanticSelectedLen, NumRefs: 0},
TypeSemanticDisabled: {Size: TypeSemanticDisabledLen, NumRefs: 0},
TypeSemanticEnabled: {Size: TypeSemanticEnabledLen, NumRefs: 0},
TypeSnippet: {Size: TypeSnippetLen, NumRefs: 2},
TypeSelection: {Size: TypeSelectionLen, NumRefs: 1},
TypeActionInput: {Size: TypeActionInputLen, NumRefs: 0},
}
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 {
+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:])
}
+2 -2
View File
@@ -8,7 +8,7 @@ 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
# Kinds
Only events that match a specified list of types are delivered to a handler.
@@ -20,7 +20,7 @@ Leave, or Scroll):
pointer.InputOp{
Tag: h,
Types: pointer.Press | pointer.Drag | pointer.Release,
Kinds: pointer.Press | pointer.Drag | pointer.Release,
}.Add(ops)
Cancel events are always delivered.
+16 -14
View File
@@ -18,7 +18,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 +32,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,7 +53,7 @@ type PassOp struct {
type PassStack struct {
ops *ops.Ops
id ops.StackID
macroID int
macroID uint32
}
// InputOp declares an input handler ready for pointer
@@ -61,8 +63,8 @@ type InputOp struct {
// Grab, if set, request that the handler get
// Grabbed priority.
Grab bool
// Types is a bitwise-or of event types to receive.
Types Type
// Kinds is a bitwise-or of event types to receive.
Kinds Kind
// ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy
//
@@ -73,8 +75,8 @@ type InputOp struct {
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 +171,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) >> 1
// Press of a pointer.
Press
// Release of a pointer.
@@ -243,7 +245,7 @@ func (op InputOp) Add(o *op.Ops) {
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 {
if op.Kinds>>16 > 0 {
panic(fmt.Errorf("value in Types overflows uint16"))
}
data := ops.Write1(&o.Internal, ops.TypePointerInputLen, op.Tag)
@@ -252,19 +254,19 @@ func (op InputOp) Add(o *op.Ops) {
data[1] = 1
}
bo := binary.LittleEndian
bo.PutUint16(data[2:], uint16(op.Types))
bo.PutUint16(data[2:], uint16(op.Kinds))
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 +277,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"
+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"},
+2 -7
View File
@@ -30,11 +30,6 @@ func TestKeyWakeup(t *testing.T) {
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) {
@@ -279,7 +274,7 @@ func TestFocusScroll(t *testing.T) {
key.InputOp{Tag: h}.Add(ops)
pointer.InputOp{
Tag: h,
Types: pointer.Scroll,
Kinds: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100),
}.Add(ops)
// Test that h is scrolled even if behind another handler.
@@ -305,7 +300,7 @@ func TestFocusClick(t *testing.T) {
key.InputOp{Tag: h}.Add(ops)
pointer.InputOp{
Tag: h,
Types: pointer.Press | pointer.Release,
Kinds: pointer.Press | pointer.Release,
}.Add(ops)
cl.Pop()
r.Frame(ops)
+21 -21
View File
@@ -66,7 +66,7 @@ type pointerHandler struct {
area int
active bool
wantsGrab bool
types pointer.Type
types pointer.Kind
// min and max horizontal/vertical scroll
scrollRange image.Rectangle
@@ -242,7 +242,7 @@ func (c *pointerCollector) newHandler(tag event.Tag, events *handlerEvents) *poi
c.q.handlers[tag] = h
// Cancel handlers on (each) first appearance, but don't
// trigger redraw.
events.AddNoRedraw(tag, pointer.Event{Type: pointer.Cancel})
events.AddNoRedraw(tag, pointer.Event{Kind: pointer.Cancel})
}
h.active = true
h.area = areaID
@@ -268,16 +268,16 @@ func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.content.tag = op.Tag
if op.Types&(pointer.Press|pointer.Release) != 0 {
if op.Kinds&(pointer.Press|pointer.Release) != 0 {
area.semantic.content.gestures |= ClickGesture
}
if op.Types&pointer.Scroll != 0 {
if op.Kinds&pointer.Scroll != 0 {
area.semantic.content.gestures |= ScrollGesture
}
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.types = h.types | op.Kinds
h.scrollRange = op.ScrollBounds
}
@@ -309,11 +309,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) {
@@ -602,7 +602,7 @@ func (q *pointerQueue) Frame(events *handlerEvents) {
func (q *pointerQueue) dropHandler(events *handlerEvents, tag event.Tag) {
if events != nil {
events.Add(tag, pointer.Event{Type: pointer.Cancel})
events.Add(tag, pointer.Event{Kind: pointer.Cancel})
}
for i := range q.pointers {
p := &q.pointers[i]
@@ -649,11 +649,11 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
continue
}
h := q.handlers[n.tag]
if e.Type&h.types == 0 {
if e.Kind&h.types == 0 {
continue
}
e := e
if e.Type == pointer.Scroll {
if e.Kind == pointer.Scroll {
if sx == 0 && sy == 0 {
break
}
@@ -663,7 +663,7 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
}
e.Position = q.invTransform(h.area, e.Position)
events.Add(n.tag, e)
if e.Type != pointer.Scroll {
if e.Kind != pointer.Scroll {
break
}
}
@@ -683,7 +683,7 @@ func (q *pointerQueue) SemanticArea(areaIdx int) (semanticContent, int) {
}
func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
if e.Type == pointer.Cancel {
if e.Kind == pointer.Cancel {
q.pointers = q.pointers[:0]
for k := range q.handlers {
q.dropHandler(events, k)
@@ -694,14 +694,14 @@ func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
p := &q.pointers[pidx]
p.last = e
switch e.Type {
switch e.Kind {
case pointer.Press:
q.deliverEnterLeaveEvents(p, events, e)
p.pressed = true
q.deliverEvent(p, events, 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)
@@ -735,7 +735,7 @@ func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e poi
var sx, sy = e.Scroll.X, e.Scroll.Y
for _, k := range p.handlers {
h := q.handlers[k]
if e.Type == pointer.Scroll {
if e.Kind == pointer.Scroll {
if sx == 0 && sy == 0 {
return
}
@@ -743,7 +743,7 @@ func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e poi
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 {
if e.Kind&h.types == 0 {
continue
}
e := e
@@ -758,7 +758,7 @@ func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e poi
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
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)
@@ -790,9 +790,9 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
continue
}
h := q.handlers[k]
e.Type = pointer.Leave
e.Kind = pointer.Leave
if e.Type&h.types != 0 {
if e.Kind&h.types != 0 {
e := e
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
@@ -804,9 +804,9 @@ func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEv
if _, found := searchTag(p.entered, k); found {
continue
}
e.Type = pointer.Enter
e.Kind = pointer.Enter
if e.Type&h.types != 0 {
if e.Kind&h.types != 0 {
e := e
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
+100 -105
View File
@@ -33,11 +33,6 @@ func TestPointerWakeup(t *testing.T) {
}
// However, adding a handler queues a Cancel event.
assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel)
// Verify that r.Events does trigger a redraw.
r.Frame(&ops)
if _, wake := r.WakeupTime(); !wake {
t.Errorf("pointer.Cancel event didn't trigger a redraw")
}
}
func TestPointerDrag(t *testing.T) {
@@ -50,12 +45,12 @@ func TestPointerDrag(t *testing.T) {
r.Queue(
// Press.
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
// Move outside the area.
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(150, 150),
},
)
@@ -72,12 +67,12 @@ func TestPointerDragNegative(t *testing.T) {
r.Queue(
// Press.
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(-50, -50),
},
// Move outside the area.
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(-150, -150),
},
)
@@ -92,15 +87,15 @@ func TestPointerGrab(t *testing.T) {
types := pointer.Press | pointer.Release
pointer.InputOp{Tag: handler1, Types: types, Grab: true}.Add(&ops)
pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
pointer.InputOp{Tag: handler3, Types: types}.Add(&ops)
pointer.InputOp{Tag: handler1, Kinds: types, Grab: true}.Add(&ops)
pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops)
pointer.InputOp{Tag: handler3, Kinds: types}.Add(&ops)
var r Router
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
)
@@ -110,7 +105,7 @@ func TestPointerGrab(t *testing.T) {
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(50, 50),
},
)
@@ -126,15 +121,15 @@ func TestPointerGrabSameHandlerTwice(t *testing.T) {
types := pointer.Press | pointer.Release
pointer.InputOp{Tag: handler1, Types: types, Grab: true}.Add(&ops)
pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
pointer.InputOp{Tag: handler1, Kinds: types, Grab: true}.Add(&ops)
pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops)
pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops)
var r Router
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
)
@@ -143,7 +138,7 @@ func TestPointerGrabSameHandlerTwice(t *testing.T) {
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(50, 50),
},
)
@@ -160,10 +155,10 @@ func TestPointerMove(t *testing.T) {
// Handler 1 area: (0, 0) - (100, 100)
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops)
// Handler 2 area: (50, 50) - (100, 100) (areas intersect).
r2 := clip.Rect(image.Rect(50, 50, 200, 200)).Push(&ops)
pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops)
r2.Pop()
r1.Pop()
@@ -172,21 +167,21 @@ func TestPointerMove(t *testing.T) {
r.Queue(
// Hit both handlers.
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
// Hit handler 1.
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(49, 50),
},
// Hit no handlers.
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(100, 50),
},
pointer.Event{
Type: pointer.Cancel,
Kind: pointer.Cancel,
},
)
assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel)
@@ -199,7 +194,7 @@ func TestPointerTypes(t *testing.T) {
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
pointer.InputOp{
Tag: handler,
Types: pointer.Press | pointer.Release,
Kinds: pointer.Press | pointer.Release,
}.Add(&ops)
r1.Pop()
@@ -207,15 +202,15 @@ func TestPointerTypes(t *testing.T) {
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(150, 150),
},
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(150, 150),
},
)
@@ -268,14 +263,14 @@ func TestPointerPriority(t *testing.T) {
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
pointer.InputOp{
Tag: handler1,
Types: pointer.Scroll,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 100}},
}.Add(&ops)
r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops)
pointer.InputOp{
Tag: handler2,
Types: pointer.Scroll,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
}.Add(&ops)
r2.Pop()
@@ -284,7 +279,7 @@ func TestPointerPriority(t *testing.T) {
r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops)
pointer.InputOp{
Tag: handler3,
Types: pointer.Scroll,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
}.Add(&ops)
r3.Pop()
@@ -294,25 +289,25 @@ func TestPointerPriority(t *testing.T) {
r.Queue(
// Hit handler 1 and 2.
pointer.Event{
Type: pointer.Scroll,
Kind: pointer.Scroll,
Position: f32.Pt(50, 25),
Scroll: f32.Pt(50, 0),
},
// Hit handler 1.
pointer.Event{
Type: pointer.Scroll,
Kind: pointer.Scroll,
Position: f32.Pt(50, 75),
Scroll: f32.Pt(50, 50),
},
// Hit handler 3.
pointer.Event{
Type: pointer.Scroll,
Kind: pointer.Scroll,
Position: f32.Pt(50, 150),
Scroll: f32.Pt(-30, -30),
},
// Hit no handlers.
pointer.Event{
Type: pointer.Scroll,
Kind: pointer.Scroll,
Position: f32.Pt(50, 225),
},
)
@@ -348,7 +343,7 @@ func TestPointerEnterLeave(t *testing.T) {
// Hit both handlers.
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
@@ -361,7 +356,7 @@ func TestPointerEnterLeave(t *testing.T) {
// Leave the second area by moving into the first.
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(45, 45),
},
)
@@ -372,7 +367,7 @@ func TestPointerEnterLeave(t *testing.T) {
// Move, but stay within the same hit area.
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(40, 40),
},
)
@@ -382,7 +377,7 @@ func TestPointerEnterLeave(t *testing.T) {
// Move outside of both inputs.
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(300, 300),
},
)
@@ -392,7 +387,7 @@ func TestPointerEnterLeave(t *testing.T) {
// Check that a Press event generates Enter Events.
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(125, 125),
},
)
@@ -403,12 +398,12 @@ func TestPointerEnterLeave(t *testing.T) {
r.Queue(
// Leave
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(25, 25),
},
// Enter
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
@@ -418,7 +413,7 @@ func TestPointerEnterLeave(t *testing.T) {
// Check that a Release event generates Enter/Leave Events.
r.Queue(
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(25,
25),
},
@@ -446,15 +441,15 @@ func TestMultipleAreas(t *testing.T) {
// Hit first area, then second area, then both.
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(25, 25),
},
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(150, 150),
},
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
@@ -470,11 +465,11 @@ func TestPointerEnterLeaveNested(t *testing.T) {
// Handler 1 area: (0, 0) - (100, 100)
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
pointer.InputOp{Tag: handler1, Types: types}.Add(&ops)
pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops)
// Handler 2 area: (25, 25) - (75, 75) (nested within first).
r2 := clip.Rect(image.Rect(25, 25, 75, 75)).Push(&ops)
pointer.InputOp{Tag: handler2, Types: types}.Add(&ops)
pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops)
r2.Pop()
r1.Pop()
@@ -483,7 +478,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
// Hit both handlers.
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
@@ -495,7 +490,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
// Leave the second area by moving into the first.
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(20, 20),
},
)
@@ -505,7 +500,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
// Move, but stay within the same hit area.
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(10, 10),
},
)
@@ -515,7 +510,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
// Move outside of both inputs.
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(200, 200),
},
)
@@ -525,7 +520,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
// Check that a Press event generates Enter Events.
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
)
@@ -535,7 +530,7 @@ func TestPointerEnterLeaveNested(t *testing.T) {
// Check that a Release event generates Enter/Leave Events.
r.Queue(
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(20, 20),
},
)
@@ -554,7 +549,7 @@ func TestPointerActiveInputDisappears(t *testing.T) {
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(25, 25),
},
)
@@ -565,7 +560,7 @@ func TestPointerActiveInputDisappears(t *testing.T) {
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(25, 25),
},
)
@@ -587,21 +582,21 @@ func TestMultitouch(t *testing.T) {
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: h1pt,
PointerID: p1,
},
)
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: h2pt,
PointerID: p2,
},
)
r.Queue(
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Position: h2pt,
PointerID: p2,
},
@@ -634,7 +629,7 @@ func TestCursor(t *testing.T) {
_at := func(x, y float32) pointer.Event {
return pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Position: f32.Pt(x, y),
@@ -734,14 +729,14 @@ func TestPassOp(t *testing.T) {
h1, h2, h3, h4 := new(int), new(int), new(int), new(int)
area := clip.Rect(image.Rect(0, 0, 100, 100))
root := area.Push(&ops)
pointer.InputOp{Tag: h1, Types: pointer.Press}.Add(&ops)
pointer.InputOp{Tag: h1, Kinds: pointer.Press}.Add(&ops)
child1 := area.Push(&ops)
pointer.InputOp{Tag: h2, Types: pointer.Press}.Add(&ops)
pointer.InputOp{Tag: h2, Kinds: pointer.Press}.Add(&ops)
child1.Pop()
child2 := area.Push(&ops)
pass := pointer.PassOp{}.Push(&ops)
pointer.InputOp{Tag: h3, Types: pointer.Press}.Add(&ops)
pointer.InputOp{Tag: h4, Types: pointer.Press}.Add(&ops)
pointer.InputOp{Tag: h3, Kinds: pointer.Press}.Add(&ops)
pointer.InputOp{Tag: h4, Kinds: pointer.Press}.Add(&ops)
pass.Pop()
child2.Pop()
root.Pop()
@@ -750,7 +745,7 @@ func TestPassOp(t *testing.T) {
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
},
)
assertEventPointerTypeSequence(t, r.Events(h1), pointer.Cancel, pointer.Press)
@@ -763,13 +758,13 @@ func TestAreaPassthrough(t *testing.T) {
var ops op.Ops
h := new(int)
pointer.InputOp{Tag: h, Types: pointer.Press}.Add(&ops)
pointer.InputOp{Tag: h, Kinds: pointer.Press}.Add(&ops)
clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
var r Router
r.Frame(&ops)
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
},
)
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press)
@@ -780,7 +775,7 @@ func TestEllipse(t *testing.T) {
h := new(int)
cl := clip.Ellipse(image.Rect(0, 0, 100, 100)).Push(&ops)
pointer.InputOp{Tag: h, Types: pointer.Press}.Add(&ops)
pointer.InputOp{Tag: h, Kinds: pointer.Press}.Add(&ops)
cl.Pop()
var r Router
r.Frame(&ops)
@@ -788,15 +783,15 @@ func TestEllipse(t *testing.T) {
// Outside ellipse.
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Press,
Kind: pointer.Press,
},
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
},
// Inside ellipse.
pointer.Event{
Position: f32.Pt(50, 50),
Type: pointer.Press,
Kind: pointer.Press,
},
)
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press)
@@ -825,7 +820,7 @@ func TestTransfer(t *testing.T) {
return src, tgt
}
// Cancel is received when the pointer is first seen.
cancel := pointer.Event{Type: pointer.Cancel}
cancel := pointer.Event{Kind: pointer.Cancel}
t.Run("transfer.Offer should panic on nil Data", func(t *testing.T) {
defer func() {
@@ -845,11 +840,11 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Press,
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
)
assertEventSequence(t, r.Events(src), cancel)
@@ -859,11 +854,11 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(30, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
pointer.Event{
Position: f32.Pt(30, 10),
Type: pointer.Release,
Kind: pointer.Release,
},
)
assertEventSequence(t, r.Events(src), transfer.CancelEvent{})
@@ -886,11 +881,11 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Press,
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
)
assertEventSequence(t, r.Events(src), cancel)
@@ -907,11 +902,11 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Press,
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
)
assertEventSequence(t, r.Events(src), cancel)
@@ -921,7 +916,7 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(40, 10),
Type: pointer.Release,
Kind: pointer.Release,
},
)
assertEventSequence(t, r.Events(src), transfer.CancelEvent{})
@@ -944,11 +939,11 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Press,
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
)
assertEventSequence(t, r.Events(src), cancel)
@@ -958,7 +953,7 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(40, 10),
Type: pointer.Release,
Kind: pointer.Release,
},
)
assertEventSequence(t, r.Events(src), transfer.RequestEvent{Type: "file"})
@@ -1011,15 +1006,15 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Press,
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
pointer.Event{
Position: f32.Pt(40, 10),
Type: pointer.Release,
Kind: pointer.Release,
},
)
ofr := &offer{data: "hello"}
@@ -1054,15 +1049,15 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Press,
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
pointer.Event{
Position: f32.Pt(40, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
)
assertEventPointerTypeSequence(t, r.Events(&hover), pointer.Cancel, pointer.Enter)
@@ -1071,7 +1066,7 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(40, 10),
Type: pointer.Release,
Kind: pointer.Release,
},
)
@@ -1102,15 +1097,15 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Press,
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
pointer.Event{
Position: f32.Pt(40, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
)
assertEventPointerTypeSequence(t, r.Events(&hover), pointer.Cancel)
@@ -1119,7 +1114,7 @@ func TestTransfer(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(40, 10),
Type: pointer.Release,
Kind: pointer.Release,
},
)
@@ -1167,7 +1162,7 @@ func TestPassCursor(t *testing.T) {
r.Frame(&ops)
r.Queue(pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Move,
Kind: pointer.Move,
})
if got := r.Cursor(); want != got {
t.Errorf("got cursor %v, want %v", got, want)
@@ -1192,18 +1187,18 @@ func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) {
defer clip.Rect(area).Push(ops).Pop()
pointer.InputOp{
Tag: tag,
Types: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave,
Kinds: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave,
}.Add(ops)
}
// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes
// that all input events are of underlying type pointer.Event, and thus will
// panic if some are not.
func pointerTypes(events []event.Event) []pointer.Type {
var types []pointer.Type
func pointerTypes(events []event.Event) []pointer.Kind {
var types []pointer.Kind
for _, e := range events {
if e, ok := e.(pointer.Event); ok {
types = append(types, e.Type)
types = append(types, e.Kind)
}
}
return types
@@ -1211,7 +1206,7 @@ func pointerTypes(events []event.Event) []pointer.Type {
// assertEventPointerTypeSequence checks that the provided events match the expected pointer event types
// in the provided order.
func assertEventPointerTypeSequence(t *testing.T, events []event.Event, expected ...pointer.Type) {
func assertEventPointerTypeSequence(t *testing.T, events []event.Event, expected ...pointer.Kind) {
t.Helper()
got := pointerTypes(events)
if !reflect.DeepEqual(got, expected) {
@@ -1239,7 +1234,7 @@ func eventsToString(evs []event.Event) string {
for _, ev := range evs {
switch e := ev.(type) {
case pointer.Event:
s = append(s, fmt.Sprintf("%T{%s}", e, e.Type.String()))
s = append(s, fmt.Sprintf("%T{%s}", e, e.Kind.String()))
default:
s = append(s, fmt.Sprintf("{%T}", e))
}
@@ -1308,7 +1303,7 @@ func BenchmarkRouterAdd(b *testing.B) {
Push(&ops)
pointer.InputOp{
Tag: handlers[i],
Types: pointer.Move,
Kinds: pointer.Move,
}.Add(&ops)
}
var r Router
@@ -1318,7 +1313,7 @@ func BenchmarkRouterAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
r.Queue(
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(50, 50),
},
)
+7 -17
View File
@@ -273,7 +273,7 @@ func (q *Router) ScrollFocus(dist image.Point) {
}
area := q.key.queue.AreaFor(focus)
q.pointer.queue.Deliver(area, pointer.Event{
Type: pointer.Scroll,
Kind: pointer.Scroll,
Source: pointer.Touch,
Scroll: f32internal.FPt(dist),
}, &q.handlers)
@@ -317,9 +317,9 @@ func (q *Router) ClickFocus() {
Source: pointer.Touch,
}
area := q.key.queue.AreaFor(focus)
e.Type = pointer.Press
e.Kind = pointer.Press
q.pointer.queue.Deliver(area, e, &q.handlers)
e.Type = pointer.Release
e.Kind = pointer.Release
q.pointer.queue.Deliver(area, e, &q.handlers)
}
@@ -438,7 +438,7 @@ func (q *Router) collect() {
op := pointer.InputOp{
Tag: encOp.Refs[0].(event.Tag),
Grab: encOp.Data[1] != 0,
Types: pointer.Type(bo.Uint16(encOp.Data[2:])),
Kinds: pointer.Kind(bo.Uint16(encOp.Data[2:])),
ScrollBounds: image.Rectangle{
Min: image.Point{
X: int(int32(bo.Uint32(encOp.Data[4:]))),
@@ -546,11 +546,11 @@ func (q *Router) collect() {
} else {
pc.semanticSelected(false)
}
case ops.TypeSemanticDisabled:
case ops.TypeSemanticEnabled:
if encOp.Data[1] != 0 {
pc.semanticDisabled(true)
pc.semanticEnabled(true)
} else {
pc.semanticDisabled(false)
pc.semanticEnabled(false)
}
}
}
@@ -593,16 +593,6 @@ func (h *handlerEvents) HadEvents() bool {
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
+2 -2
View File
@@ -74,11 +74,11 @@ 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)
pointer.InputOp{Tag: new(int), Kinds: pointer.Press | pointer.Release}.Add(&ops)
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
r.Frame(&ops)
+6 -6
View File
@@ -36,8 +36,8 @@ 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.Write1String(&o.Internal, ops.TypeSemanticLabelLen, string(l))
@@ -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
}
}
+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),
+1 -1
View File
@@ -144,7 +144,7 @@ func (l *List) Dragging() bool {
}
func (l *List) update(gtx Context) {
d := l.scroll.Scroll(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
d := l.scroll.Update(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
l.scrollDelta = d
l.Position.Offset += d
}
+9 -9
View File
@@ -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}
}
+1 -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
+1 -1
View File
@@ -111,7 +111,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
}
+14 -1
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
@@ -48,7 +60,7 @@ type PaintOp struct {
// until Pop is called.
type OpacityStack struct {
id ops.StackID
macroID int
macroID uint32
ops *ops.Ops
}
@@ -103,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) {
+1
View File
@@ -284,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)
+19 -20
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) {
+3 -3
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
}
@@ -469,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,7 +505,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
if endOfCluster {
glyph.Flags |= FlagClusterBreak
if run.truncator {
glyph.Runes += l.txt.unreadRuneCount
glyph.Runes += uint16(l.txt.unreadRuneCount)
}
} else {
glyph.Runes = 0
+7 -7
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++
@@ -191,9 +191,9 @@ func TestShapingNewlineHandling(t *testing.T) {
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
glyphs = append(glyphs, g)
if g.Flags&FlagTruncator == 0 {
runes += g.Runes
runes += int(g.Runes)
} else {
truncated += g.Runes
truncated += int(g.Runes)
}
}
if expected := len([]rune(tc.textInput)) - tc.expectedTruncated; expected != runes {
@@ -571,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)
+9 -12
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(gtx) {
b.Value = !b.Value
changed = true
}
return changed
}
@@ -43,13 +43,10 @@ func (b *Bool) History() []Press {
}
func (b *Bool) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions {
b.Update(gtx)
dims := b.clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
for b.clk.Clicked() {
b.Value = !b.Value
b.changed = true
}
semantic.SelectedOp(b.Value).Add(gtx.Ops)
semantic.DisabledOp(gtx.Queue == nil).Add(gtx.Ops)
semantic.EnabledOp(gtx.Queue != nil).Add(gtx.Ops)
return w(gtx)
})
return dims
+59 -65
View File
@@ -17,18 +17,16 @@ 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
// clicks is for saved clicks to support Clicked.
clicks []Click
history []Press
keyTag struct{}
requestFocus bool
focused bool
pressedKey string
keyTag struct{}
requestFocus bool
requestClicks int
focused bool
pressedKey string
}
// Click represents a click.
@@ -50,26 +48,24 @@ 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
// Clicked reports whether there are pending clicks. If so, Clicked
// removes the earliest click.
func (b *Clickable) Clicked(gtx layout.Context) bool {
if len(b.clicks) > 0 {
b.clicks = b.clicks[1:]
return true
}
n := copy(b.clicks, b.clicks[1:])
b.clicks = b.clicks[:n]
if b.prevClicks > 0 {
b.prevClicks--
b.clicks = b.Update(gtx)
if len(b.clicks) > 0 {
b.clicks = b.clicks[1:]
return true
}
return true
return false
}
// Hovered reports whether a pointer is over the element.
@@ -92,44 +88,44 @@ 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)
b.Update(gtx)
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)
enabled := gtx.Queue != nil
semantic.EnabledOp(enabled).Add(gtx.Ops)
b.click.Add(gtx.Ops)
if !disabled {
if enabled {
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
}
c.Add(gtx.Ops)
return dims
}
// Update the button state by processing events, and return the resulting
// clicks, if any.
func (b *Clickable) Update(gtx layout.Context) []Click {
b.clicks = nil
if gtx.Queue == nil {
b.focused = false
}
if b.requestFocus {
key.FocusOp{Tag: &b.keyTag}.Add(gtx.Ops)
b.requestFocus = false
}
for len(b.history) > 0 {
c := b.history[0]
if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
@@ -138,34 +134,31 @@ 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,
})
var clicks []Click
if c := b.requestClicks; c > 0 {
b.requestClicks = 0
clicks = append(clicks, Click{
NumClicks: c,
})
}
for _, e := range b.click.Update(gtx) {
switch e.Kind {
case gesture.KindClick:
if l := len(b.history); l > 0 {
b.history[l-1].End = gtx.Now
}
case gesture.TypeCancel:
clicks = append(clicks, Click{
Modifiers: e.Modifiers,
NumClicks: e.NumClicks,
})
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)
}
@@ -198,11 +191,12 @@ func (b *Clickable) update(gtx layout.Context) {
}
// only register a key as a click if the key was pressed and released while this button was focused
b.pressedKey = ""
b.clicks = append(b.clicks, Click{
clicks = append(clicks, Click{
Modifiers: e.Modifiers,
NumClicks: 1,
})
}
}
}
return clicks
}
+6 -6
View File
@@ -48,6 +48,7 @@ func TestClickable(t *testing.T) {
t.Error("button 2 should not have focus")
}
// frame: press & release return
frame()
r.Queue(
key.Event{
Name: key.NameReturn,
@@ -58,11 +59,10 @@ func TestClickable(t *testing.T) {
State: key.Release,
},
)
frame()
if !b1.Clicked() {
if !b1.Clicked(gtx) {
t.Error("button 1 did not get clicked when it got return press & release")
}
if b2.Clicked() {
if b2.Clicked(gtx) {
t.Error("button 2 got clicked when it did not have focus")
}
// frame: press return down
@@ -73,7 +73,7 @@ func TestClickable(t *testing.T) {
},
)
frame()
if b1.Clicked() {
if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it only got return press")
}
// frame: request focus for button 2
@@ -95,10 +95,10 @@ func TestClickable(t *testing.T) {
},
)
frame()
if b1.Clicked() {
if b1.Clicked(gtx) {
t.Error("button 1 got clicked, even if it had lost focus")
}
if b2.Clicked() {
if b2.Clicked(gtx) {
t.Error("button 2 should not have been clicked, as it only got return release")
}
}
+28 -22
View File
@@ -12,12 +12,11 @@ import (
// Decorations handles the states of window decorations.
type Decorations struct {
clicks []Clickable
clicks map[int]*Clickable
resize [8]struct {
gesture.Hover
gesture.Drag
}
actions system.Action
maximized bool
}
@@ -35,22 +34,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 +56,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.
+26 -37
View File
@@ -17,41 +17,16 @@ 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
handle struct{}
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 {
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)
@@ -64,7 +39,7 @@ func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dim
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,13 +52,27 @@ 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 := range d.drag.Update(gtx.Metric, gtx.Queue, gesture.Both) {
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 _, ev := range gtx.Queue.Events(&d.handle) {
if e, ok := ev.(transfer.RequestEvent); ok {
return e.Type, true
}
}
return "", false
}
// Offer the data ready for a drop. Must be called after being Requested.
+3 -3
View File
@@ -39,15 +39,15 @@ func TestDraggable(t *testing.T) {
r.Queue(
pointer.Event{
Position: f32.Pt(10, 10),
Type: pointer.Press,
Kind: pointer.Press,
},
pointer.Event{
Position: f32.Pt(20, 10),
Type: pointer.Move,
Kind: pointer.Move,
},
pointer.Event{
Position: f32.Pt(20, 10),
Type: pointer.Release,
Kind: pointer.Release,
},
)
ofr := &offer{data: "hello"}
+19 -16
View File
@@ -225,7 +225,7 @@ func (e *Editor) processPointer(gtx layout.Context) {
axis = gesture.Vertical
smin, smax = sbounds.Min.Y, sbounds.Max.Y
}
sdist := e.scroller.Scroll(gtx.Metric, gtx, gtx.Now, axis)
sdist := e.scroller.Update(gtx.Metric, gtx, gtx.Now, axis)
var soff int
if e.SingleLine {
e.text.ScrollRel(sdist, 0)
@@ -238,8 +238,8 @@ func (e *Editor) processPointer(gtx layout.Context) {
switch evt := evt.(type) {
case gesture.ClickEvent:
switch {
case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse,
evt.Type == gesture.TypeClick && evt.Source != pointer.Mouse:
case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
prevCaretPos, _ := e.text.Selection()
e.blinkStart = gtx.Now
e.text.MoveCoord(image.Point{
@@ -278,10 +278,10 @@ func (e *Editor) processPointer(gtx layout.Context) {
case pointer.Event:
release := false
switch {
case evt.Type == pointer.Release && evt.Source == pointer.Mouse:
case evt.Kind == pointer.Release && evt.Source == pointer.Mouse:
release = true
fallthrough
case evt.Type == pointer.Drag && evt.Source == pointer.Mouse:
case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse:
if e.dragging {
e.blinkStart = gtx.Now
e.text.MoveCoord(image.Point{
@@ -305,10 +305,10 @@ func (e *Editor) processPointer(gtx layout.Context) {
func (e *Editor) clickDragEvents(gtx layout.Context) []event.Event {
var combinedEvents []event.Event
for _, evt := range e.clicker.Events(gtx) {
for _, evt := range e.clicker.Update(gtx) {
combinedEvents = append(combinedEvents, evt)
}
for _, evt := range e.dragger.Events(gtx.Metric, gtx, gesture.Both) {
for _, evt := range e.dragger.Update(gtx.Metric, gtx, gesture.Both) {
combinedEvents = append(combinedEvents, evt)
}
return combinedEvents
@@ -517,15 +517,10 @@ func (e *Editor) initBuffer() {
e.text.WrapPolicy = e.WrapPolicy
}
// Layout lays out the editor using the provided textMaterial as the paint material
// for the text glyphs+caret and the selectMaterial as the paint material for the
// selection rectangle.
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
// Update the state of the editor in response to input events.
func (e *Editor) Update(gtx layout.Context) {
e.initBuffer()
e.text.Update(gtx, lt, font, size, e.processEvents)
dims := e.layout(gtx, textMaterial, selectMaterial)
e.processEvents(gtx)
if e.focused {
// Notify IME of selection if it changed.
newSel := e.ime.selection
@@ -551,8 +546,16 @@ func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, siz
e.updateSnippet(gtx, e.ime.start, e.ime.end)
}
}
return dims
// Layout lays out the editor using the provided textMaterial as the paint material
// for the text glyphs+caret and the selectMaterial as the paint material for the
// selection rectangle.
func (e *Editor) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectMaterial op.CallOp) layout.Dimensions {
e.Update(gtx)
e.text.Layout(gtx, lt, font, size)
return e.layout(gtx, textMaterial, selectMaterial)
}
// updateSnippet adds a key.SnippetOp if the snippet content or position
+11 -54
View File
@@ -139,7 +139,7 @@ func TestEditorReadOnly(t *testing.T) {
// Type some new characters.
gtx.Ops.Reset()
gtx.Queue = &testQueue{events: []event.Event{key.EditEvent{Range: key.Range{Start: cStart2, End: cEnd2}, Text: "something else"}}}
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.Update(gtx)
textContent2 := e.Text()
if textContent2 != textContent {
t.Errorf("readonly editor modified by key.EditEvent")
@@ -159,22 +159,22 @@ func TestEditorReadOnly(t *testing.T) {
gtx.Ops.Reset()
gtx.Queue = &testQueue{events: []event.Event{
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Buttons: pointer.ButtonPrimary,
Position: f32.Pt(float32(dims.Size.X)*.5, 5),
},
pointer.Event{
Type: pointer.Drag,
Kind: pointer.Drag,
Buttons: pointer.ButtonPrimary,
Position: layout.FPt(dims.Size).Mul(.5),
},
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Buttons: pointer.ButtonPrimary,
Position: layout.FPt(dims.Size).Mul(.5),
},
}}
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.Update(gtx)
cStart3, cEnd3 := e.Selection()
if cStart3 == cStart2 || cEnd3 == cEnd2 {
t.Errorf("expected mouse interaction to change selection.")
@@ -285,44 +285,10 @@ func TestEditor(t *testing.T) {
e.MoveCaret(-3, -3)
assertCaret(t, e, 1, 1, len("æbc\na"))
e.text.Mask = '*'
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.Update(gtx)
assertCaret(t, e, 1, 1, len("æbc\na"))
e.MoveCaret(-3, -3)
assertCaret(t, e, 0, 2, len("æb"))
/*
NOTE(whereswaldon): it isn't possible to check the raw glyph data
like this anymore. How should we handle this?
e.Mask = '\U0001F92B'
e.Layout(gtx, cache, font, fontSize, op.CallOp{},op.CallOp{})
e.moveEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc"))
// When a password mask is applied, it should replace all visible glyphs
spaces := 0
for _, r := range textSample {
if unicode.IsSpace(r) {
spaces++
}
}
nonSpaces := len([]rune(textSample)) - spaces
glyphCounts := make(map[int]int)
// This loop assumes a single-run text, which we know is safe here.
for _, line := range e.lines {
for _, glyph := range line.Runs[0].Glyphs {
glyphCounts[int(glyph.ID)]++
}
}
if len(glyphCounts) > 2 {
t.Errorf("masked text contained glyphs other than mask and whitespace")
}
for gid, count := range glyphCounts {
if count != spaces && count != nonSpaces {
t.Errorf("glyph with id %d occurred %d times, expected either %d or %d", gid, count, spaces, nonSpaces)
}
}
*/
// Test that moveLine applies x offsets from previous moves.
e.SetText("long line\nshort")
e.SetCaret(0, 0)
@@ -680,11 +646,8 @@ func TestEditorMoveWord(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: english,
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e.SetText(t)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.Update(gtx)
return e
}
for ii, tt := range tests {
@@ -785,11 +748,8 @@ func TestEditorInsert(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: english,
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e.SetText(t)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.Update(gtx)
return e
}
for ii, tt := range tests {
@@ -875,11 +835,8 @@ func TestEditorDeleteWord(t *testing.T) {
Constraints: layout.Exact(image.Pt(100, 100)),
Locale: english,
}
cache := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
fontSize := unit.Sp(10)
font := font.Font{}
e.SetText(t)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.Update(gtx)
return e
}
for ii, tt := range tests {
@@ -947,13 +904,13 @@ g 2 4 6 8 g
events: []event.Event{
pointer.Event{
Buttons: pointer.ButtonPrimary,
Type: pointer.Press,
Kind: pointer.Press,
Source: pointer.Mouse,
Time: tim,
Position: f32.Pt(textWidth(e, startPos.lineCol.line, 0, startPos.lineCol.col), textBaseline(e, startPos.lineCol.line)),
},
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Source: pointer.Mouse,
Time: tim,
Position: f32.Pt(textWidth(e, endPos.lineCol.line, 0, endPos.lineCol.col), textBaseline(e, endPos.lineCol.line)),
+53 -54
View File
@@ -20,8 +20,6 @@ type Enum struct {
focus string
focused bool
changed bool
keys []*enumKey
}
@@ -40,11 +38,55 @@ func (e *Enum) index(k string) *enumKey {
return nil
}
// Changed reports whether Value has changed by user interaction since the last
// call to Changed.
func (e *Enum) Changed() bool {
changed := e.changed
e.changed = false
// Update the state and report whether Value has changed by user interaction.
func (e *Enum) Update(gtx layout.Context) bool {
if gtx.Queue == nil {
e.focused = false
}
e.hovering = false
changed := false
for _, state := range e.keys {
for _, ev := range state.click.Update(gtx) {
switch ev.Kind {
case gesture.KindPress:
if ev.Source == pointer.Mouse {
key.FocusOp{Tag: &state.tag}.Add(gtx.Ops)
}
case gesture.KindClick:
if state.key != e.Value {
e.Value = state.key
changed = true
}
}
}
for _, ev := range gtx.Events(&state.tag) {
switch ev := ev.(type) {
case key.FocusEvent:
if ev.Focus {
e.focused = true
e.focus = state.key
} else if state.key == e.focus {
e.focused = false
}
case key.Event:
if !e.focused || ev.State != key.Release {
break
}
if ev.Name != key.NameReturn && ev.Name != key.NameSpace {
break
}
if state.key != e.Value {
e.Value = state.key
changed = true
}
}
}
if state.click.Hovered() {
e.hovered = state.key
e.hovering = true
}
}
return changed
}
@@ -60,6 +102,7 @@ func (e *Enum) Focused() (string, bool) {
// Layout adds the event handler for the key k.
func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layout.Dimensions {
e.Update(gtx)
m := op.Record(gtx.Ops)
dims := content(gtx)
c := m.Stop()
@@ -73,57 +116,13 @@ func (e *Enum) Layout(gtx layout.Context, k string, content layout.Widget) layou
e.keys = append(e.keys, state)
}
clk := &state.click
for _, ev := range clk.Events(gtx) {
switch ev.Type {
case gesture.TypePress:
if ev.Source == pointer.Mouse {
key.FocusOp{Tag: &state.tag}.Add(gtx.Ops)
}
case gesture.TypeClick:
if state.key != e.Value {
e.Value = state.key
e.changed = true
}
}
}
for _, ev := range gtx.Events(&state.tag) {
switch ev := ev.(type) {
case key.FocusEvent:
if ev.Focus {
e.focused = true
e.focus = state.key
} else if state.key == e.focus {
e.focused = false
}
case key.Event:
if !e.focused || ev.State != key.Release {
break
}
if ev.Name != key.NameReturn && ev.Name != key.NameSpace {
break
}
if state.key != e.Value {
e.Value = state.key
e.changed = true
}
}
}
if clk.Hovered() {
e.hovered = k
e.hovering = true
} else if e.hovered == k {
e.hovering = false
}
clk.Add(gtx.Ops)
disabled := gtx.Queue == nil
if !disabled {
enabled := gtx.Queue != nil
if enabled {
key.InputOp{Tag: &state.tag, Keys: "⏎|Space"}.Add(gtx.Ops)
} else if e.focus == k {
e.focused = false
}
semantic.SelectedOp(k == e.Value).Add(gtx.Ops)
semantic.DisabledOp(disabled).Add(gtx.Ops)
semantic.EnabledOp(enabled).Add(gtx.Ops)
c.Add(gtx.Ops)
return dims
+8 -10
View File
@@ -47,23 +47,21 @@ func ExampleClickable_passthrough() {
pointer.Event{
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
pointer.Event{
Source: pointer.Mouse,
Buttons: pointer.ButtonPrimary,
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(50, 50),
},
)
// The second layout ensures that the click event is registered by the buttons.
widget()
if button1.Clicked() {
if button1.Clicked(gtx) {
fmt.Println("button1 clicked!")
}
if button2.Clicked() {
if button2.Clicked(gtx) {
fmt.Println("button2 clicked!")
}
@@ -95,7 +93,7 @@ func ExampleDraggable_Layout() {
drag.Layout(gtx, w, w)
// drag must respond with an Offer event when requested.
// Use the drag method for this.
if m, ok := drag.Requested(); ok {
if m, ok := drag.Update(gtx); ok {
drag.Offer(gtx.Ops, m, offer{Data: "hello world"})
}
@@ -125,15 +123,15 @@ func ExampleDraggable_Layout() {
// Send drag and drop gesture events.
r.Queue(
pointer.Event{
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(5, 5), // in the drag area
},
pointer.Event{
Type: pointer.Move,
Kind: pointer.Move,
Position: f32.Pt(5, 5), // in the drop area
},
pointer.Event{
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(30, 30), // in the drop area
},
)
+29 -63
View File
@@ -9,59 +9,29 @@ import (
"gioui.org/io/pointer"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/unit"
)
// Float is for selecting a value in a range.
type Float struct {
// Value is the value of the Float, in the [0; 1] range.
Value float32
drag gesture.Drag
pos float32 // position normalized to [0, 1]
length float32
changed bool
drag gesture.Drag
axis layout.Axis
length float32
}
// Dragging returns whether the value is being interacted with.
func (f *Float) Dragging() bool { return f.drag.Dragging() }
// Layout updates the value according to drag events along the f's main axis.
//
// The range of f is set by the minimum constraints main axis value.
func (f *Float) Layout(gtx layout.Context, axis layout.Axis, min, max float32, invert bool, pointerMargin int) layout.Dimensions {
func (f *Float) Layout(gtx layout.Context, axis layout.Axis, pointerMargin unit.Dp) layout.Dimensions {
f.Update(gtx)
size := gtx.Constraints.Min
f.length = float32(axis.Convert(size).X)
f.axis = axis
var de *pointer.Event
for _, e := range f.drag.Events(gtx.Metric, gtx, gesture.Axis(axis)) {
if e.Type == pointer.Press || e.Type == pointer.Drag {
de = &e
}
}
value := f.Value
if de != nil {
xy := de.Position.X
if axis == layout.Vertical {
xy = f.length - de.Position.Y
}
if invert {
xy = f.length - xy
}
f.pos = xy / f.length
value = min + (max-min)*f.pos
} else if min != max {
f.pos = (value - min) / (max - min)
}
// Unconditionally call setValue in case min, max, or value changed.
f.setValue(value, min, max)
if f.pos < 0 {
f.pos = 0
} else if f.pos > 1 {
f.pos = 1
}
margin := axis.Convert(image.Pt(pointerMargin, 0))
margin := axis.Convert(image.Pt(gtx.Dp(pointerMargin), 0))
rect := image.Rectangle{
Min: margin.Mul(-1),
Max: size.Add(margin),
@@ -72,30 +42,26 @@ func (f *Float) Layout(gtx layout.Context, axis layout.Axis, min, max float32, i
return layout.Dimensions{Size: size}
}
func (f *Float) setValue(value, min, max float32) {
if min > max {
min, max = max, min
// Update the Value according to drag events along the f's main axis.
// The return value reports whether the value was changed.
//
// The range of f is set by the minimum constraints main axis value.
func (f *Float) Update(gtx layout.Context) bool {
changed := false
for _, e := range f.drag.Update(gtx.Metric, gtx, gesture.Axis(f.axis)) {
if f.length > 0 && (e.Kind == pointer.Press || e.Kind == pointer.Drag) {
pos := e.Position.X
if f.axis == layout.Vertical {
pos = f.length - e.Position.Y
}
f.Value = pos / f.length
if f.Value < 0 {
f.Value = 0
} else if f.Value > 1 {
f.Value = 1
}
changed = true
}
}
if value < min {
value = min
} else if value > max {
value = max
}
if f.Value != value {
f.Value = value
f.changed = true
}
}
// Pos reports the selected position.
func (f *Float) Pos() float32 {
return f.pos * f.length
}
// Changed reports whether the value has changed since
// the last call to Changed.
func (f *Float) Changed() bool {
changed := f.changed
f.changed = false
return changed
}
+5 -5
View File
@@ -134,17 +134,17 @@ type textIterator struct {
// processGlyph checks whether the glyph is visible within the iterator's configured
// viewport and (if so) updates the iterator's text dimensions to include the glyph.
func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visibleOrBefore bool) {
func (it *textIterator) processGlyph(g text.Glyph, ok bool) (visibleOrBefore bool) {
if it.maxLines > 0 {
if g.Flags&text.FlagTruncator != 0 && g.Flags&text.FlagClusterBreak != 0 {
// A glyph carrying both of these flags provides the count of truncated runes.
it.truncated = g.Runes
it.truncated = int(g.Runes)
}
if g.Flags&text.FlagLineBreak != 0 {
it.linesSeen++
}
if it.linesSeen == it.maxLines && g.Flags&text.FlagParagraphBreak != 0 {
return g, false
return false
}
}
// Compute the maximum extent to which glyphs overhang on the horizontal
@@ -191,7 +191,7 @@ func (it *textIterator) processGlyph(g text.Glyph, ok bool) (_ text.Glyph, visib
it.bounds.Max.X = max(it.bounds.Max.X, logicalBounds.Max.X)
it.bounds.Max.Y = max(it.bounds.Max.Y, logicalBounds.Max.Y)
}
return g, ok && !below
return ok && !below
}
func fixedToFloat(i fixed.Int26_6) float32 {
@@ -206,7 +206,7 @@ func fixedToFloat(i fixed.Int26_6) float32 {
// This design is awkward, but prevents the line slice from escaping
// to the heap.
func (it *textIterator) paintGlyph(gtx layout.Context, shaper *text.Shaper, glyph text.Glyph, line []text.Glyph) ([]text.Glyph, bool) {
_, visibleOrBefore := it.processGlyph(glyph, true)
visibleOrBefore := it.processGlyph(glyph, true)
if it.visible {
if len(line) == 0 {
it.lineOff = f32.Point{X: fixedToFloat(glyph.X), Y: float32(glyph.Y)}.Sub(layout.FPt(it.viewport.Min))
+1 -4
View File
@@ -146,10 +146,7 @@ func TestGlyphIterator(t *testing.T) {
glyphs := getGlyphs(16, 0, maxWidth, text.Start, tc.str)
it := textIterator{viewport: tc.viewport, maxLines: tc.maxLines}
for i, g := range glyphs {
gOut, ok := it.processGlyph(g, true)
if gOut != g {
t.Errorf("textIterator modified glyphs[%d], original:\n%#+v, modified:\n%#+v", i, g, gOut)
}
ok := it.processGlyph(g, true)
if !ok && i != tc.stopAtGlyph {
t.Errorf("expected iterator to stop at glyph %d, stopped at %d", tc.stopAtGlyph, i)
}
+5 -5
View File
@@ -61,8 +61,8 @@ func (s *Scrollbar) Layout(gtx layout.Context, axis layout.Axis, viewportStart,
}
// Jump to a click in the track.
for _, event := range s.track.Events(gtx) {
if event.Type != gesture.TypeClick ||
for _, event := range s.track.Update(gtx) {
if event.Kind != gesture.KindClick ||
event.Modifiers != key.Modifiers(0) ||
event.NumClicks > 1 {
continue
@@ -80,8 +80,8 @@ func (s *Scrollbar) Layout(gtx layout.Context, axis layout.Axis, viewportStart,
}
// Offset to account for any drags.
for _, event := range s.drag.Events(gtx.Metric, gtx, gesture.Axis(axis)) {
switch event.Type {
for _, event := range s.drag.Update(gtx.Metric, gtx, gesture.Axis(axis)) {
switch event.Kind {
case pointer.Drag:
case pointer.Release, pointer.Cancel:
s.dragging = false
@@ -136,7 +136,7 @@ func (s *Scrollbar) Layout(gtx layout.Context, axis layout.Axis, viewportStart,
// Process events from the indicator so that hover is
// detected properly.
_ = s.indicator.Events(gtx)
_ = s.indicator.Update(gtx)
return layout.Dimensions{}
}
+14 -18
View File
@@ -93,9 +93,8 @@ func IconButton(th *Theme, button *widget.Clickable, icon *widget.Icon, descript
func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) layout.Dimensions {
return button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
constraints := gtx.Constraints
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
if button.Hovered() || button.Focused() {
paint.Fill(gtx.Ops, f32color.Hovered(color.NRGBA{}))
@@ -104,11 +103,8 @@ func Clickable(gtx layout.Context, button *widget.Clickable, w layout.Widget) la
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
gtx.Constraints = constraints
return w(gtx)
}),
},
w,
)
})
}
@@ -131,8 +127,8 @@ func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Di
min := gtx.Constraints.Min
return b.Button.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
rr := gtx.Dp(b.CornerRadius)
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
background := b.Background
@@ -147,11 +143,11 @@ func (b ButtonLayoutStyle) Layout(gtx layout.Context, w layout.Widget) layout.Di
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
},
func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min = min
return layout.Center.Layout(gtx, w)
}),
},
)
})
}
@@ -163,8 +159,8 @@ func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
if d := b.Description; d != "" {
semantic.DescriptionOp(b.Description).Add(gtx.Ops)
}
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
rr := (gtx.Constraints.Min.X + gtx.Constraints.Min.Y) / 4
defer clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops).Pop()
background := b.Background
@@ -179,8 +175,8 @@ func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
},
func(gtx layout.Context) layout.Dimensions {
return b.Inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
size := gtx.Dp(b.Size)
if b.Icon != nil {
@@ -191,7 +187,7 @@ func (b IconButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
Size: image.Point{X: size, Y: size},
}
})
}),
},
)
})
c := m.Stop()
+5 -5
View File
@@ -88,18 +88,18 @@ func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimension
cl := d.Decorations.Clickable(a)
dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
return layout.Stack{Alignment: layout.Center}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
for _, c := range cl.History() {
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
},
func(gtx layout.Context) layout.Dimensions {
paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
return inset.Layout(gtx, w)
}),
},
)
})
size.X += dims.Size.X
+6 -10
View File
@@ -165,12 +165,9 @@ func (s ScrollbarStyle) layout(gtx layout.Context, axis layout.Axis, viewportSta
if axis == layout.Horizontal {
inset.Top, inset.Bottom, inset.Left, inset.Right = inset.Left, inset.Right, inset.Top, inset.Bottom
}
// Capture the outer constraints because layout.Stack will reset
// the minimum to zero.
outerConstraints := gtx.Constraints
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
// Lay out the draggable track underneath the scroll indicator.
area := image.Rectangle{
Max: gtx.Constraints.Min,
@@ -186,10 +183,9 @@ func (s ScrollbarStyle) layout(gtx layout.Context, axis layout.Axis, viewportSta
s.Scrollbar.AddTrack(gtx.Ops)
paint.FillShape(gtx.Ops, s.Track.Color, clip.Rect(area).Op())
return layout.Dimensions{}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
gtx.Constraints = outerConstraints
return layout.Dimensions{Size: gtx.Constraints.Min}
},
func(gtx layout.Context) layout.Dimensions {
return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
// Use axis-independent constraints.
gtx.Constraints.Min = axis.Convert(gtx.Constraints.Min)
@@ -231,7 +227,7 @@ func (s ScrollbarStyle) layout(gtx layout.Context, axis layout.Axis, viewportSta
return layout.Dimensions{Size: axis.Convert(gtx.Constraints.Min)}
})
}),
},
)
}
+1
View File
@@ -38,6 +38,7 @@ func RadioButton(th *Theme, group *widget.Enum, key, label string) RadioButtonSt
// Layout updates enum and displays the radio button.
func (r RadioButtonStyle) Layout(gtx layout.Context) layout.Dimensions {
r.Group.Update(gtx)
hovered, hovering := r.Group.Hovered()
focus, focused := r.Group.Focused()
return r.Group.Layout(gtx, r.Key, func(gtx layout.Context) layout.Dimensions {
+17 -20
View File
@@ -16,10 +16,8 @@ import (
)
// Slider is for selecting a value in a range.
func Slider(th *Theme, float *widget.Float, min, max float32) SliderStyle {
func Slider(th *Theme, float *widget.Float) SliderStyle {
return SliderStyle{
Min: min,
Max: max,
Color: th.Palette.ContrastBg,
Float: float,
FingerSize: th.FingerSize,
@@ -27,35 +25,34 @@ func Slider(th *Theme, float *widget.Float, min, max float32) SliderStyle {
}
type SliderStyle struct {
Axis layout.Axis
Min, Max float32
Invert bool
Color color.NRGBA
Float *widget.Float
Axis layout.Axis
Color color.NRGBA
Float *widget.Float
FingerSize unit.Dp
}
func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
thumbRadius := gtx.Dp(6)
const thumbRadius unit.Dp = 6
tr := gtx.Dp(thumbRadius)
trackWidth := gtx.Dp(2)
axis := s.Axis
// Keep a minimum length so that the track is always visible.
minLength := thumbRadius + 3*thumbRadius + thumbRadius
minLength := tr + 3*tr + tr
// Try to expand to finger size, but only if the constraints
// allow for it.
touchSizePx := min(gtx.Dp(s.FingerSize), axis.Convert(gtx.Constraints.Max).Y)
sizeMain := max(axis.Convert(gtx.Constraints.Min).X, minLength)
sizeCross := max(2*thumbRadius, touchSizePx)
sizeCross := max(2*tr, touchSizePx)
size := axis.Convert(image.Pt(sizeMain, sizeCross))
o := axis.Convert(image.Pt(thumbRadius, 0))
o := axis.Convert(image.Pt(tr, 0))
trans := op.Offset(o).Push(gtx.Ops)
gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*thumbRadius, sizeCross))
s.Float.Layout(gtx, axis, s.Min, s.Max, s.Invert, thumbRadius)
gtx.Constraints.Min = axis.Convert(image.Pt(sizeMain-2*tr, sizeCross))
dims := s.Float.Layout(gtx, axis, thumbRadius)
gtx.Constraints.Min = gtx.Constraints.Min.Add(axis.Convert(image.Pt(0, sizeCross)))
thumbPos := thumbRadius + int(s.Float.Pos())
thumbPos := tr + int(s.Float.Value*float32(axis.Convert(dims.Size).X))
trans.Pop()
color := s.Color
@@ -65,7 +62,7 @@ func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
rect := func(minx, miny, maxx, maxy int) image.Rectangle {
r := image.Rect(minx, miny, maxx, maxy)
if s.Invert != (axis == layout.Vertical) {
if axis == layout.Vertical {
r.Max.X, r.Min.X = sizeMain-r.Min.X, sizeMain-r.Max.X
}
r.Min = axis.Convert(r.Min)
@@ -75,7 +72,7 @@ func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
// Draw track before thumb.
track := rect(
thumbRadius, sizeCross/2-trackWidth/2,
tr, sizeCross/2-trackWidth/2,
thumbPos, sizeCross/2+trackWidth/2,
)
paint.FillShape(gtx.Ops, color, clip.Rect(track).Op())
@@ -83,15 +80,15 @@ func (s SliderStyle) Layout(gtx layout.Context) layout.Dimensions {
// Draw track after thumb.
track = rect(
thumbPos, axis.Convert(track.Min).Y,
sizeMain-thumbRadius, axis.Convert(track.Max).Y,
sizeMain-tr, axis.Convert(track.Max).Y,
)
paint.FillShape(gtx.Ops, f32color.MulAlpha(color, 96), clip.Rect(track).Op())
// Draw thumb.
pt := image.Pt(thumbPos, sizeCross/2)
thumb := rect(
pt.X-thumbRadius, pt.Y-thumbRadius,
pt.X+thumbRadius, pt.Y+thumbRadius,
pt.X-tr, pt.Y-tr,
pt.X+tr, pt.Y+tr,
)
paint.FillShape(gtx.Ops, color, clip.Ellipse(thumb).Op(gtx.Ops))
+1
View File
@@ -39,6 +39,7 @@ func Switch(th *Theme, swtch *widget.Bool, description string) SwitchStyle {
// Layout updates the switch and displays it.
func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions {
s.Switch.Update(gtx)
trackWidth := gtx.Dp(36)
trackHeight := gtx.Dp(16)
thumbSize := gtx.Dp(20)
+16 -10
View File
@@ -182,25 +182,31 @@ func (l *Selectable) Truncated() bool {
return l.text.Truncated()
}
// Update the state of the selectable in response to input events.
func (l *Selectable) Update(gtx layout.Context) {
l.initialize()
l.handleEvents(gtx)
}
// Layout clips to the dimensions of the selectable, updates the shaped text, configures input handling, and paints
// the text and selection rectangles. The provided textMaterial and selectionMaterial ops are used to set the
// paint material for the text and selection rectangles, respectively.
func (l *Selectable) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, textMaterial, selectionMaterial op.CallOp) layout.Dimensions {
l.initialize()
l.Update(gtx)
l.text.LineHeight = l.LineHeight
l.text.LineHeightScale = l.LineHeightScale
l.text.Alignment = l.Alignment
l.text.MaxLines = l.MaxLines
l.text.Truncator = l.Truncator
l.text.WrapPolicy = l.WrapPolicy
l.text.Update(gtx, lt, font, size, l.handleEvents)
l.text.Layout(gtx, lt, font, size)
dims := l.text.Dimensions()
defer clip.Rect(image.Rectangle{Max: dims.Size}).Push(gtx.Ops).Pop()
pointer.CursorText.Add(gtx.Ops)
var keys key.Set
if l.focused {
const keyFilterAllArrows = "(ShortAlt)-(Shift)-[←,→,↑,↓]|(Shift)-[⏎,⌤]|(ShortAlt)-(Shift)-[⌫,⌦]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,V,X,A]|Short-(Shift)-Z"
keys = keyFilterAllArrows
const keyFilter = "(ShortAlt)-(Shift)-[←,→,↑,↓]|(Shift)-[⇞,⇟,⇱,⇲]|Short-[C,X,A]"
keys = keyFilter
}
key.InputOp{Tag: l, Keys: keys}.Add(gtx.Ops)
if l.requestFocus {
@@ -236,8 +242,8 @@ func (e *Selectable) processPointer(gtx layout.Context) {
switch evt := evt.(type) {
case gesture.ClickEvent:
switch {
case evt.Type == gesture.TypePress && evt.Source == pointer.Mouse,
evt.Type == gesture.TypeClick && evt.Source != pointer.Mouse:
case evt.Kind == gesture.KindPress && evt.Source == pointer.Mouse,
evt.Kind == gesture.KindClick && evt.Source != pointer.Mouse:
prevCaretPos, _ := e.text.Selection()
e.text.MoveCoord(image.Point{
X: int(math.Round(float64(evt.Position.X))),
@@ -271,10 +277,10 @@ func (e *Selectable) processPointer(gtx layout.Context) {
case pointer.Event:
release := false
switch {
case evt.Type == pointer.Release && evt.Source == pointer.Mouse:
case evt.Kind == pointer.Release && evt.Source == pointer.Mouse:
release = true
fallthrough
case evt.Type == pointer.Drag && evt.Source == pointer.Mouse:
case evt.Kind == pointer.Drag && evt.Source == pointer.Mouse:
if e.dragging {
e.text.MoveCoord(image.Point{
X: int(math.Round(float64(evt.Position.X))),
@@ -292,10 +298,10 @@ func (e *Selectable) processPointer(gtx layout.Context) {
func (e *Selectable) clickDragEvents(gtx layout.Context) []event.Event {
var combinedEvents []event.Event
for _, evt := range e.clicker.Events(gtx) {
for _, evt := range e.clicker.Update(gtx) {
combinedEvents = append(combinedEvents, evt)
}
for _, evt := range e.dragger.Events(gtx.Metric, gtx, gesture.Both) {
for _, evt := range e.dragger.Update(gtx.Metric, gtx, gesture.Both) {
combinedEvents = append(combinedEvents, evt)
}
return combinedEvents
+10 -11
View File
@@ -228,10 +228,8 @@ func (e *textView) calculateViewSize(gtx layout.Context) image.Point {
return gtx.Constraints.Constrain(base)
}
// Update the text, reshaping it as necessary. If not nil, eventHandling will be invoked after reshaping the text to
// allow parent widgets to adapt to any changes in text content or positioning. If eventHandling modifies the contents
// of the textView, it is guaranteed to be reshaped (and ready for painting) before Update returns.
func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp, eventHandling func(gtx layout.Context)) {
// Layout the text, reshaping it as necessary.
func (e *textView) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, size unit.Sp) {
if e.params.Locale != gtx.Locale {
e.params.Locale = gtx.Locale
e.invalidate()
@@ -289,10 +287,6 @@ func (e *textView) Update(gtx layout.Context, lt *text.Shaper, font font.Font, s
}
e.makeValid()
if eventHandling != nil {
eventHandling(gtx)
e.makeValid()
}
if viewSize := e.calculateViewSize(gtx); viewSize != e.viewSize {
e.viewSize = viewSize
@@ -492,14 +486,19 @@ func (e *textView) layoutText(lt *text.Shaper) {
it := textIterator{viewport: image.Rectangle{Max: image.Point{X: math.MaxInt, Y: math.MaxInt}}}
if lt != nil {
lt.Layout(e.params, r)
for glyph, ok := it.processGlyph(lt.NextGlyph()); ok; glyph, ok = it.processGlyph(lt.NextGlyph()) {
e.index.Glyph(glyph)
for {
g, ok := lt.NextGlyph()
if !it.processGlyph(g, ok) {
break
}
e.index.Glyph(g)
}
} else {
// Make a fake glyph for every rune in the reader.
b := bufio.NewReader(r)
for _, _, err := b.ReadRune(); err != io.EOF; _, _, err = b.ReadRune() {
g, _ := it.processGlyph(text.Glyph{Runes: 1, Flags: text.FlagClusterBreak}, true)
g := text.Glyph{Runes: 1, Flags: text.FlagClusterBreak}
_ = it.processGlyph(g, true)
e.index.Glyph(g)
}
}
+6 -9
View File
@@ -7,7 +7,6 @@ import (
"os"
"sort"
"testing"
"time"
colEmoji "eliasnaur.com/font/noto/emoji/color"
"gioui.org/font"
@@ -48,10 +47,6 @@ var (
}()
)
func init() {
rand.Seed(int64(time.Now().Nanosecond()))
}
func runBenchmarkPermutations(b *testing.B, benchmark func(b *testing.B, runes int, locale system.Locale, document string)) {
docKeys := maps.Keys(documents)
sort.Strings(docKeys)
@@ -127,11 +122,12 @@ func BenchmarkLabelDynamic(b *testing.B) {
font := font.Font{}
runes := []rune(txt)[:runeCount]
l := Label{}
r := rand.New(rand.NewSource(42))
b.ResetTimer()
for i := 0; i < b.N; i++ {
// simulate a constantly changing string
a := rand.Intn(len(runes))
b := rand.Intn(len(runes))
a := r.Intn(len(runes))
b := r.Intn(len(runes))
runes[a], runes[b] = runes[b], runes[a]
l.Layout(gtx, cache, font, fontSize, string(runes), op.CallOp{})
if render {
@@ -196,11 +192,12 @@ func BenchmarkEditorDynamic(b *testing.B) {
runes := []rune(txt)[:runeCount]
e := Editor{}
e.SetText(string(runes))
r := rand.New(rand.NewSource(42))
b.ResetTimer()
for i := 0; i < b.N; i++ {
// simulate a constantly changing string
a := rand.Intn(e.Len())
b := rand.Intn(e.Len())
a := r.Intn(e.Len())
b := r.Intn(e.Len())
e.SetCaret(a, a+1)
takeStr := e.SelectedText()
e.Insert("")
+2 -2
View File
@@ -35,12 +35,12 @@ func TestBool(t *testing.T) {
r.Queue(
pointer.Event{
Source: pointer.Touch,
Type: pointer.Press,
Kind: pointer.Press,
Position: f32.Pt(50, 50),
},
pointer.Event{
Source: pointer.Touch,
Type: pointer.Release,
Kind: pointer.Release,
Position: f32.Pt(50, 50),
},
)