47 Commits

Author SHA1 Message Date
Elias Naur 8e47316332 app: [Windows] suppress double-click behaviour for custom decorations
Fixes: https://todo.sr.ht/~eliasnaur/gio/600
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-07-15 13:01:41 +08:00
Chris Waldon 55ae5c5b84 app: [Wayland] prevent recursive scroll event processing
This commit zeroes the accumulated scroll distance on the window before invoking the
event delivery code, since the event delivery code is able to call back into the scroll
processing. Prior to this change, the callback could re-processing the scroll delta
while magnifying it by a factor of 10.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This was observed and is not theoretical.

Windows 8 9200

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-03-06 20:49:44 +00:00
52 changed files with 769 additions and 450 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ environment:
tasks: tasks:
- install_go: | - install_go: |
mkdir -p /home/build/sdk mkdir -p /home/build/sdk
curl -s https://dl.google.com/go/go1.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - curl -s https://dl.google.com/go/go1.22.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- prepare_toolchain: | - prepare_toolchain: |
mkdir -p $APPLE_TOOLCHAIN_ROOT mkdir -p $APPLE_TOOLCHAIN_ROOT
cd $APPLE_TOOLCHAIN_ROOT cd $APPLE_TOOLCHAIN_ROOT
+1 -1
View File
@@ -16,7 +16,7 @@ environment:
tasks: tasks:
- install_go: | - install_go: |
mkdir -p /home/build/sdk mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.19.11.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf - curl https://dl.google.com/go/go1.22.2.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- test_gio: | - test_gio: |
cd gio cd gio
go test ./... go test ./...
+3 -1
View File
@@ -40,7 +40,7 @@ secrets:
tasks: tasks:
- install_go: | - install_go: |
mkdir -p /home/build/sdk mkdir -p /home/build/sdk
curl -s https://dl.google.com/go/go1.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf - curl -s https://dl.google.com/go/go1.22.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- check_gofmt: | - check_gofmt: |
cd gio cd gio
test -z "$(gofmt -s -l .)" test -z "$(gofmt -s -l .)"
@@ -80,6 +80,8 @@ tasks:
unzip -q ndk.zip unzip -q ndk.zip
rm ndk.zip rm ndk.zip
mv android-ndk-* ndk-bundle mv android-ndk-* ndk-bundle
# sdkmanager needs lots of file descriptors
ulimit -n 10000
yes|sdkmanager --licenses yes|sdkmanager --licenses
sdkmanager "platforms;android-31" "build-tools;32.0.0" sdkmanager "platforms;android-31" "build-tools;32.0.0"
- test_android: | - test_android: |
+1 -1
View File
@@ -10,7 +10,7 @@ environment:
tasks: tasks:
- install_go: | - install_go: |
mkdir -p /home/build/sdk mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.19.11.src.tar.gz | tar -C /home/build/sdk -xzf - curl https://dl.google.com/go/go1.22.2.src.tar.gz | tar -C /home/build/sdk -xzf -
cd /home/build/sdk/go/src cd /home/build/sdk/go/src
./make.bash ./make.bash
- test_gio: | - test_gio: |
+4
View File
@@ -61,6 +61,10 @@ type FrameEvent struct {
type ViewEvent interface { type ViewEvent interface {
implementsViewEvent() implementsViewEvent()
ImplementsEvent() ImplementsEvent()
// Valid will return true when the ViewEvent does contains valid handles.
// If a window receives an invalid ViewEvent, it should deinitialize any
// state referring to handles from a previous ViewEvent.
Valid() bool
} }
// Insets is the space taken up by // Insets is the space taken up by
+4 -6
View File
@@ -17,9 +17,8 @@ import (
) )
type androidContext struct { type androidContext struct {
win *window win *window
eglSurf egl.NativeWindowType eglSurf egl.NativeWindowType
width, height int
*egl.Context *egl.Context
} }
@@ -45,9 +44,8 @@ func (c *androidContext) Refresh() error {
if err := c.win.setVisual(c.Context.VisualID()); err != nil { if err := c.win.setVisual(c.Context.VisualID()); err != nil {
return err return err
} }
win, width, height := c.win.nativeWindow() win, _, _ := c.win.nativeWindow()
c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win)) c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win))
c.width, c.height = width, height
return nil return nil
} }
@@ -55,7 +53,7 @@ func (c *androidContext) Lock() error {
// The Android emulator creates a broken surface if it is not // The Android emulator creates a broken surface if it is not
// created on the same thread as the context is made current. // created on the same thread as the context is made current.
if c.eglSurf != nil { if c.eglSurf != nil {
if err := c.Context.CreateSurface(c.eglSurf, c.width, c.height); err != nil { if err := c.Context.CreateSurface(c.eglSurf); err != nil {
return err return err
} }
c.eglSurf = nil c.eglSurf = nil
+1 -1
View File
@@ -69,7 +69,7 @@ func (c *wlContext) Refresh() error {
} }
c.eglWin = eglWin c.eglWin = eglWin
eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin))) eglSurf := egl.NativeWindowType(uintptr(unsafe.Pointer(eglWin)))
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil { if err := c.Context.CreateSurface(eglSurf); err != nil {
return err return err
} }
if err := c.Context.MakeCurrent(); err != nil { if err := c.Context.MakeCurrent(); err != nil {
+12 -17
View File
@@ -5,8 +5,6 @@
package app package app
import ( import (
"golang.org/x/sys/windows"
"gioui.org/internal/egl" "gioui.org/internal/egl"
) )
@@ -24,6 +22,18 @@ func init() {
if err != nil { if err != nil {
return nil, err return nil, err
} }
win, _, _ := w.HWND()
eglSurf := egl.NativeWindowType(win)
if err := ctx.CreateSurface(eglSurf); err != nil {
ctx.Release()
return nil, err
}
if err := ctx.MakeCurrent(); err != nil {
ctx.Release()
return nil, err
}
defer ctx.ReleaseCurrent()
ctx.EnableVSync(true)
return &glContext{win: w, Context: ctx}, nil return &glContext{win: w, Context: ctx}, nil
}, },
}) })
@@ -37,21 +47,6 @@ func (c *glContext) Release() {
} }
func (c *glContext) Refresh() error { func (c *glContext) Refresh() error {
c.Context.ReleaseSurface()
var (
win windows.Handle
width, height int
)
win, width, height = c.win.HWND()
eglSurf := egl.NativeWindowType(win)
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
return err
}
c.Context.EnableVSync(true)
c.Context.ReleaseCurrent()
return nil return nil
} }
+12 -11
View File
@@ -25,6 +25,18 @@ func init() {
if err != nil { if err != nil {
return nil, err return nil, err
} }
win, _, _ := w.window()
eglSurf := egl.NativeWindowType(uintptr(win))
if err := ctx.CreateSurface(eglSurf); err != nil {
ctx.Release()
return nil, err
}
if err := ctx.MakeCurrent(); err != nil {
ctx.Release()
return nil, err
}
defer ctx.ReleaseCurrent()
ctx.EnableVSync(true)
return &x11Context{win: w, Context: ctx}, nil return &x11Context{win: w, Context: ctx}, nil
} }
} }
@@ -37,17 +49,6 @@ func (c *x11Context) Release() {
} }
func (c *x11Context) Refresh() error { func (c *x11Context) Refresh() error {
c.Context.ReleaseSurface()
win, width, height := c.win.window()
eglSurf := egl.NativeWindowType(uintptr(win))
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
return err
}
defer c.Context.ReleaseCurrent()
c.Context.EnableVSync(true)
return nil return nil
} }
+1 -1
View File
@@ -7,7 +7,7 @@
#include <OpenGL/OpenGL.h> #include <OpenGL/OpenGL.h>
#include "_cgo_export.h" #include "_cgo_export.h"
CALayer *gio_layerFactory(void) { CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool { @autoreleasepool {
return [CALayer layer]; return [CALayer layer];
} }
+1
View File
@@ -266,6 +266,7 @@ const (
WM_MOUSEWHEEL = 0x020A WM_MOUSEWHEEL = 0x020A
WM_MOUSEHWHEEL = 0x020E WM_MOUSEHWHEEL = 0x020E
WM_NCACTIVATE = 0x0086 WM_NCACTIVATE = 0x0086
WM_NCLBUTTONDBLCLK = 0x00A3
WM_NCHITTEST = 0x0084 WM_NCHITTEST = 0x0084
WM_NCCALCSIZE = 0x0083 WM_NCCALCSIZE = 0x0083
WM_PAINT = 0x000F WM_PAINT = 0x000F
+2 -1
View File
@@ -60,8 +60,9 @@ static void presentDrawable(CFTypeRef queueRef, CFTypeRef drawableRef) {
id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef; id<MTLDrawable> drawable = (__bridge id<MTLDrawable>)drawableRef;
id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef; id<MTLCommandQueue> queue = (__bridge id<MTLCommandQueue>)queueRef;
id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer]; id<MTLCommandBuffer> cmdBuffer = [queue commandBuffer];
[cmdBuffer presentDrawable:drawable];
[cmdBuffer commit]; [cmdBuffer commit];
[cmdBuffer waitUntilScheduled];
[drawable present];
} }
} }
+4 -1
View File
@@ -21,7 +21,10 @@ Class gio_layerClass(void) {
static CFTypeRef getMetalLayer(CFTypeRef viewRef) { static CFTypeRef getMetalLayer(CFTypeRef viewRef) {
@autoreleasepool { @autoreleasepool {
UIView *view = (__bridge UIView *)viewRef; UIView *view = (__bridge UIView *)viewRef;
return CFBridgingRetain(view.layer); CAMetalLayer *l = (CAMetalLayer *)view.layer;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = YES;
return CFBridgingRetain(l);
} }
} }
+6 -2
View File
@@ -12,9 +12,13 @@ package app
#import <QuartzCore/CAMetalLayer.h> #import <QuartzCore/CAMetalLayer.h>
#include <CoreFoundation/CoreFoundation.h> #include <CoreFoundation/CoreFoundation.h>
CALayer *gio_layerFactory(void) { CALayer *gio_layerFactory(BOOL presentWithTrans) {
@autoreleasepool { @autoreleasepool {
return [CAMetalLayer layer]; CAMetalLayer *l = [CAMetalLayer layer];
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = presentWithTrans;
return l;
} }
} }
+6 -10
View File
@@ -45,6 +45,8 @@ type Config struct {
CustomRenderer bool CustomRenderer bool
// Decorated reports whether window decorations are provided automatically. // Decorated reports whether window decorations are provided automatically.
Decorated bool Decorated bool
// Focused reports whether has the keyboard focus.
Focused bool
// decoHeight is the height of the fallback decoration for platforms such // decoHeight is the height of the fallback decoration for platforms such
// as Wayland that may need fallback client-side decorations. // as Wayland that may need fallback client-side decorations.
decoHeight unit.Dp decoHeight unit.Dp
@@ -171,19 +173,13 @@ type context interface {
Unlock() Unlock()
} }
// basicDriver is the subset of [driver] that may be called even after
// a window is destroyed.
type basicDriver interface {
// Event blocks until an even is available and returns it.
Event() event.Event
// Invalidate requests a FrameEvent.
Invalidate()
}
// driver is the interface for the platform implementation // driver is the interface for the platform implementation
// of a window. // of a window.
type driver interface { type driver interface {
basicDriver // Event blocks until an event is available and returns it.
Event() event.Event
// Invalidate requests a FrameEvent.
Invalidate()
// SetAnimating sets the animation flag. When the window is animating, // SetAnimating sets the animation flag. When the window is animating,
// FrameEvents are delivered as fast as the display can handle them. // FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool) SetAnimating(anim bool)
+5 -1
View File
@@ -593,7 +593,8 @@ func Java_org_gioui_GioView_onBack(env *C.JNIEnv, class C.jclass, view C.jlong)
//export Java_org_gioui_GioView_onFocusChange //export Java_org_gioui_GioView_onFocusChange
func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) { func Java_org_gioui_GioView_onFocusChange(env *C.JNIEnv, class C.jclass, view C.jlong, focus C.jboolean) {
w := cgo.Handle(view).Value().(*window) w := cgo.Handle(view).Value().(*window)
w.processEvent(key.FocusEvent{Focus: focus == C.JNI_TRUE}) w.config.Focused = focus == C.JNI_TRUE
w.processEvent(ConfigEvent{Config: w.config})
} }
//export Java_org_gioui_GioView_onWindowInsets //export Java_org_gioui_GioView_onWindowInsets
@@ -1494,3 +1495,6 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
func (AndroidViewEvent) implementsViewEvent() {} func (AndroidViewEvent) implementsViewEvent() {}
func (AndroidViewEvent) ImplementsEvent() {} func (AndroidViewEvent) ImplementsEvent() {}
func (a AndroidViewEvent) Valid() bool {
return a != (AndroidViewEvent{})
}
+5 -1
View File
@@ -75,9 +75,13 @@ var displayLinks sync.Map
var mainFuncs = make(chan func(), 1) var mainFuncs = make(chan func(), 1)
func isMainThread() bool {
return bool(C.isMainThread())
}
// runOnMain runs the function on the main thread. // runOnMain runs the function on the main thread.
func runOnMain(f func()) { func runOnMain(f func()) {
if C.isMainThread() { if isMainThread() {
f() f()
return return
} }
+49 -2
View File
@@ -12,6 +12,7 @@ package app
#include <UIKit/UIKit.h> #include <UIKit/UIKit.h>
#include <stdint.h> #include <stdint.h>
__attribute__ ((visibility ("hidden"))) int gio_applicationMain(int argc, char *argv[]);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle); __attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
struct drawParams { struct drawParams {
@@ -21,6 +22,7 @@ struct drawParams {
}; };
static void writeClipboard(unichar *chars, NSUInteger length) { static void writeClipboard(unichar *chars, NSUInteger length) {
#if !TARGET_OS_TV
@autoreleasepool { @autoreleasepool {
NSString *s = [NSString string]; NSString *s = [NSString string];
if (length > 0) { if (length > 0) {
@@ -29,13 +31,18 @@ static void writeClipboard(unichar *chars, NSUInteger length) {
UIPasteboard *p = UIPasteboard.generalPasteboard; UIPasteboard *p = UIPasteboard.generalPasteboard;
p.string = s; p.string = s;
} }
#endif
} }
static CFTypeRef readClipboard(void) { static CFTypeRef readClipboard(void) {
#if !TARGET_OS_TV
@autoreleasepool { @autoreleasepool {
UIPasteboard *p = UIPasteboard.generalPasteboard; UIPasteboard *p = UIPasteboard.generalPasteboard;
return (__bridge_retained CFTypeRef)p.string; return (__bridge_retained CFTypeRef)p.string;
} }
#else
return nil;
#endif
} }
static void showTextInput(CFTypeRef viewRef) { static void showTextInput(CFTypeRef viewRef) {
@@ -75,6 +82,7 @@ import "C"
import ( import (
"image" "image"
"io" "io"
"os"
"runtime" "runtime"
"runtime/cgo" "runtime/cgo"
"runtime/debug" "runtime/debug"
@@ -211,7 +219,8 @@ func onDestroy(h C.uintptr_t) {
//export onFocus //export onFocus
func onFocus(h C.uintptr_t, focus int) { func onFocus(h C.uintptr_t, focus int) {
w := viewFor(h) w := viewFor(h)
w.ProcessEvent(key.FocusEvent{Focus: focus != 0}) w.config.Focused = focus != 0
w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export onLowMemory //export onLowMemory
@@ -387,13 +396,51 @@ func newWindow(win *callbacks, options []Option) {
<-mainWindow.windows <-mainWindow.windows
} }
var mainMode = mainModeUndefined
const (
mainModeUndefined = iota
mainModeExe
mainModeLibrary
)
func osMain() { func osMain() {
if !isMainThread() {
panic("app.Main must be run on the main goroutine")
}
switch mainMode {
case mainModeUndefined:
mainMode = mainModeExe
var argv []*C.char
for _, arg := range os.Args {
a := C.CString(arg)
defer C.free(unsafe.Pointer(a))
argv = append(argv, a)
}
C.gio_applicationMain(C.int(len(argv)), unsafe.SliceData(argv))
case mainModeExe:
panic("app.Main may be called only once")
case mainModeLibrary:
// Do nothing, we're embedded as a library.
}
} }
//export gio_runMain //export gio_runMain
func gio_runMain() { func gio_runMain() {
runMain() if !isMainThread() {
panic("app.Main must be run on the main goroutine")
}
switch mainMode {
case mainModeUndefined:
mainMode = mainModeLibrary
runMain()
case mainModeExe:
// Do nothing, main has already been called.
}
} }
func (UIKitViewEvent) implementsViewEvent() {} func (UIKitViewEvent) implementsViewEvent() {}
func (UIKitViewEvent) ImplementsEvent() {} func (UIKitViewEvent) ImplementsEvent() {}
func (u UIKitViewEvent) Valid() bool {
return u != (UIKitViewEvent{})
}
+25 -1
View File
@@ -26,12 +26,13 @@ CGFloat _keyboardHeight;
self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0); self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame]; UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame];
[self.view addSubview: drawView]; [self.view addSubview: drawView];
#ifndef TARGET_OS_TV #if !TARGET_OS_TV
drawView.multipleTouchEnabled = YES; drawView.multipleTouchEnabled = YES;
#endif #endif
drawView.preservesSuperviewLayoutMargins = YES; drawView.preservesSuperviewLayoutMargins = YES;
drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0); drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
onCreate((__bridge CFTypeRef)drawView, (__bridge CFTypeRef)self); onCreate((__bridge CFTypeRef)drawView, (__bridge CFTypeRef)self);
#if !TARGET_OS_TV
[[NSNotificationCenter defaultCenter] addObserver:self [[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChange:) selector:@selector(keyboardWillChange:)
name:UIKeyboardWillShowNotification name:UIKeyboardWillShowNotification
@@ -44,6 +45,7 @@ CGFloat _keyboardHeight;
selector:@selector(keyboardWillHide:) selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification name:UIKeyboardWillHideNotification
object:nil]; object:nil];
#endif
[[NSNotificationCenter defaultCenter] addObserver: self [[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(applicationDidEnterBackground:) selector: @selector(applicationDidEnterBackground:)
name: UIApplicationDidEnterBackgroundNotification name: UIApplicationDidEnterBackgroundNotification
@@ -89,6 +91,7 @@ CGFloat _keyboardHeight;
[super didReceiveMemoryWarning]; [super didReceiveMemoryWarning];
} }
#if !TARGET_OS_TV
- (void)keyboardWillChange:(NSNotification *)note { - (void)keyboardWillChange:(NSNotification *)note {
NSDictionary *userInfo = note.userInfo; NSDictionary *userInfo = note.userInfo;
CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
@@ -100,6 +103,7 @@ CGFloat _keyboardHeight;
_keyboardHeight = 0.0; _keyboardHeight = 0.0;
[self.view setNeedsLayout]; [self.view setNeedsLayout];
} }
#endif
@end @end
static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UIEvent *event) { static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UIEvent *event) {
@@ -276,3 +280,23 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
GioView *v = (__bridge GioView *)viewRef; GioView *v = (__bridge GioView *)viewRef;
v.handle = handle; v.handle = handle;
} }
@interface _gioAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end
@implementation _gioAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = controller;
[self.window makeKeyAndVisible];
return YES;
}
@end
int gio_applicationMain(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([_gioAppDelegate class]));
}
}
+7 -2
View File
@@ -258,11 +258,13 @@ func (w *window) addEventListeners() {
return nil return nil
}) })
w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.tarea, "focus", func(this js.Value, args []js.Value) interface{} {
w.processEvent(key.FocusEvent{Focus: true}) w.config.Focused = true
w.processEvent(ConfigEvent{Config: w.config})
return nil return nil
}) })
w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} { w.addEventListener(w.tarea, "blur", func(this js.Value, args []js.Value) interface{} {
w.processEvent(key.FocusEvent{Focus: false}) w.config.Focused = false
w.processEvent(ConfigEvent{Config: w.config})
w.blur() w.blur()
return nil return nil
}) })
@@ -820,3 +822,6 @@ func translateKey(k string) (key.Name, bool) {
func (JSViewEvent) implementsViewEvent() {} func (JSViewEvent) implementsViewEvent() {}
func (JSViewEvent) ImplementsEvent() {} func (JSViewEvent) ImplementsEvent() {}
func (j JSViewEvent) Valid() bool {
return !(j.Element.IsNull() || j.Element.IsUndefined())
}
+29 -5
View File
@@ -40,7 +40,7 @@ import (
#define MOUSE_SCROLL 4 #define MOUSE_SCROLL 4
__attribute__ ((visibility ("hidden"))) void gio_main(void); __attribute__ ((visibility ("hidden"))) void gio_main(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight); __attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle); __attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
@@ -195,6 +195,14 @@ static void setScreenFrame(CFTypeRef windowRef, CGFloat x, CGFloat y, CGFloat w,
} }
} }
static void resetLayerFrame(CFTypeRef viewRef) {
@autoreleasepool {
NSView* view = (__bridge NSView *)viewRef;
NSRect r = view.frame;
view.layer.frame = r;
}
}
static void hideWindow(CFTypeRef windowRef) { static void hideWindow(CFTypeRef windowRef) {
@autoreleasepool { @autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef; NSWindow* window = (__bridge NSWindow *)windowRef;
@@ -456,6 +464,9 @@ func (w *window) Configure(options []Option) {
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans) C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans) C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans) C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
// When toggling the titlebar, the layer doesn't update its frame
// until the next resize. Force it.
C.resetLayerFrame(w.view)
} }
w.ProcessEvent(ConfigEvent{Config: w.config}) w.ProcessEvent(ConfigEvent{Config: w.config})
} }
@@ -612,8 +623,9 @@ func gio_onDraw(h C.uintptr_t) {
//export gio_onFocus //export gio_onFocus
func gio_onFocus(h C.uintptr_t, focus C.int) { func gio_onFocus(h C.uintptr_t, focus C.int) {
w := windowFor(h) w := windowFor(h)
w.ProcessEvent(key.FocusEvent{Focus: focus == 1})
w.SetCursor(w.cursor) w.SetCursor(w.cursor)
w.config.Focused = focus == 1
w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export gio_onChangeScreen //export gio_onChangeScreen
@@ -913,7 +925,9 @@ func newWindow(win *callbacks, options []Option) {
w.loop = newEventLoop(w.w, w.wakeup) w.loop = newEventLoop(w.w, w.wakeup)
win.SetDriver(w) win.SetDriver(w)
res <- struct{}{} res <- struct{}{}
if err := w.init(); err != nil { var cnf Config
cnf.apply(unit.Metric{}, options)
if err := w.init(cnf.CustomRenderer); err != nil {
w.ProcessEvent(DestroyEvent{Err: err}) w.ProcessEvent(DestroyEvent{Err: err})
return return
} }
@@ -934,8 +948,12 @@ func newWindow(win *callbacks, options []Option) {
<-res <-res
} }
func (w *window) init() error { func (w *window) init(customRenderer bool) error {
view := C.gio_createView() presentWithTrans := 1
if customRenderer {
presentWithTrans = 0
}
view := C.gio_createView(C.int(presentWithTrans))
if view == 0 { if view == 0 {
return errors.New("newOSWindow: failed to create view") return errors.New("newOSWindow: failed to create view")
} }
@@ -964,6 +982,9 @@ func (w *window) init() error {
} }
func osMain() { func osMain() {
if !isMainThread() {
panic("app.Main must run on the main goroutine")
}
C.gio_main() C.gio_main()
} }
@@ -1053,3 +1074,6 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
func (AppKitViewEvent) implementsViewEvent() {} func (AppKitViewEvent) implementsViewEvent() {}
func (AppKitViewEvent) ImplementsEvent() {} func (AppKitViewEvent) ImplementsEvent() {}
func (a AppKitViewEvent) Valid() bool {
return a != (AppKitViewEvent{})
}
+21 -7
View File
@@ -6,7 +6,7 @@
#include "_cgo_export.h" #include "_cgo_export.h"
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void); __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWithTrans);
@interface GioAppDelegate : NSObject<NSApplicationDelegate> @interface GioAppDelegate : NSObject<NSApplicationDelegate>
@end @end
@@ -16,6 +16,7 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioView : NSView <CALayerDelegate,NSTextInputClient> @interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@property uintptr_t handle; @property uintptr_t handle;
@property BOOL presentWithTrans;
@end @end
@implementation GioWindowDelegate @implementation GioWindowDelegate
@@ -47,13 +48,17 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
} }
- (void)windowDidBecomeKey:(NSNotification *)notification { - (void)windowDidBecomeKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView; GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 1); if ([window firstResponder] == view) {
gio_onFocus(view.handle, 1);
}
} }
- (void)windowDidResignKey:(NSNotification *)notification { - (void)windowDidResignKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object]; NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView; GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 0); if ([window firstResponder] == view) {
gio_onFocus(view.handle, 0);
}
} }
@end @end
@@ -84,7 +89,7 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
gio_onDraw(self.handle); gio_onDraw(self.handle);
} }
- (CALayer *)makeBackingLayer { - (CALayer *)makeBackingLayer {
CALayer *layer = gio_layerFactory(); CALayer *layer = gio_layerFactory(self.presentWithTrans);
layer.delegate = self; layer.delegate = self;
return layer; return layer;
} }
@@ -205,6 +210,14 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
- (void)dealloc { - (void)dealloc {
gio_onDestroy(self.handle); gio_onDestroy(self.handle);
} }
- (BOOL) becomeFirstResponder {
gio_onFocus(self.handle, 1);
return [super becomeFirstResponder];
}
- (BOOL) resignFirstResponder {
gio_onFocus(self.handle, 0);
return [super resignFirstResponder];
}
@end @end
// Delegates are weakly referenced from their peers. Nothing // Delegates are weakly referenced from their peers. Nothing
@@ -380,10 +393,11 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
} }
} }
CFTypeRef gio_createView(void) { CFTypeRef gio_createView(int presentWithTrans) {
@autoreleasepool { @autoreleasepool {
NSRect frame = NSMakeRect(0, 0, 0, 0); NSRect frame = NSMakeRect(0, 0, 0, 0);
GioView* view = [[GioView alloc] initWithFrame:frame]; GioView* view = [[GioView alloc] initWithFrame:frame];
view.presentWithTrans = presentWithTrans ? YES : NO;
view.wantsLayer = YES; view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize; view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
+6 -24
View File
@@ -9,7 +9,6 @@ import (
"errors" "errors"
"unsafe" "unsafe"
"gioui.org/io/event"
"gioui.org/io/pointer" "gioui.org/io/pointer"
) )
@@ -22,6 +21,9 @@ type X11ViewEvent struct {
func (X11ViewEvent) implementsViewEvent() {} func (X11ViewEvent) implementsViewEvent() {}
func (X11ViewEvent) ImplementsEvent() {} func (X11ViewEvent) ImplementsEvent() {}
func (x X11ViewEvent) Valid() bool {
return x != (X11ViewEvent{})
}
type WaylandViewEvent struct { type WaylandViewEvent struct {
// Display is the *wl_display returned by wl_display_connect. // Display is the *wl_display returned by wl_display_connect.
@@ -32,6 +34,9 @@ type WaylandViewEvent struct {
func (WaylandViewEvent) implementsViewEvent() {} func (WaylandViewEvent) implementsViewEvent() {}
func (WaylandViewEvent) ImplementsEvent() {} func (WaylandViewEvent) ImplementsEvent() {}
func (w WaylandViewEvent) Valid() bool {
return w != (WaylandViewEvent{})
}
func osMain() { func osMain() {
select {} select {}
@@ -57,35 +62,12 @@ func newWindow(window *callbacks, options []Option) {
errFirst = err errFirst = err
} }
} }
window.SetDriver(&dummyDriver{
win: window,
wakeups: make(chan event.Event, 1),
})
if errFirst == nil { if errFirst == nil {
errFirst = errors.New("app: no window driver available") errFirst = errors.New("app: no window driver available")
} }
window.ProcessEvent(DestroyEvent{Err: errFirst}) window.ProcessEvent(DestroyEvent{Err: errFirst})
} }
type dummyDriver struct {
win *callbacks
wakeups chan event.Event
}
func (d *dummyDriver) Event() event.Event {
if e, ok := d.win.nextEvent(); ok {
return e
}
return <-d.wakeups
}
func (d *dummyDriver) Invalidate() {
select {
case d.wakeups <- wakeupEvent{}:
default:
}
}
// xCursor contains mapping from pointer.Cursor to XCursor. // xCursor contains mapping from pointer.Cursor to XCursor.
var xCursor = [...]string{ var xCursor = [...]string{
pointer.CursorDefault: "left_ptr", pointer.CursorDefault: "left_ptr",
+24 -26
View File
@@ -216,9 +216,7 @@ type window struct {
wakeups chan struct{} wakeups chan struct{}
// invMu avoids the race between the destruction of disp and closing bool
// Invalidate waking it up.
invMu sync.Mutex
} }
type poller struct { type poller struct {
@@ -556,7 +554,7 @@ func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface
//export gio_onToplevelClose //export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) { func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := callbackLoad(data).(*window) w := callbackLoad(data).(*window)
w.close(nil) w.closing = true
} }
//export gio_onToplevelConfigure //export gio_onToplevelConfigure
@@ -1139,7 +1137,7 @@ func (w *window) Perform(actions system.Action) {
walkActions(actions, func(action system.Action) { walkActions(actions, func(action system.Action) {
switch action { switch action {
case system.ActionClose: case system.ActionClose:
w.close(nil) w.closing = true
} }
}) })
} }
@@ -1222,7 +1220,8 @@ func gio_onKeyboardEnter(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
w := callbackLoad(unsafe.Pointer(surf)).(*window) w := callbackLoad(unsafe.Pointer(surf)).(*window)
s.keyboardFocus = w s.keyboardFocus = w
s.disp.repeat.Stop(0) s.disp.repeat.Stop(0)
w.ProcessEvent(key.FocusEvent{Focus: true}) w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export gio_onKeyboardLeave //export gio_onKeyboardLeave
@@ -1231,7 +1230,8 @@ func gio_onKeyboardLeave(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, se
s.serial = serial s.serial = serial
s.disp.repeat.Stop(0) s.disp.repeat.Stop(0)
w := s.keyboardFocus w := s.keyboardFocus
w.ProcessEvent(key.FocusEvent{Focus: false}) w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
} }
//export gio_onKeyboardKey //export gio_onKeyboardKey
@@ -1364,6 +1364,9 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
func (w *window) close(err error) { func (w *window) close(err error) {
w.ProcessEvent(WaylandViewEvent{}) w.ProcessEvent(WaylandViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err}) w.ProcessEvent(DestroyEvent{Err: err})
w.destroy()
w.disp.destroy()
w.disp = nil
} }
func (w *window) dispatch() { func (w *window) dispatch() {
@@ -1372,7 +1375,7 @@ func (w *window) dispatch() {
w.w.Invalidate() w.w.Invalidate()
return return
} }
if err := w.disp.dispatch(); err != nil { if err := w.disp.dispatch(); err != nil || w.closing {
w.close(err) w.close(err)
return return
} }
@@ -1397,13 +1400,6 @@ func (w *window) Event() event.Event {
w.dispatch() w.dispatch()
continue continue
} }
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
w.invMu.Lock()
w.disp.destroy()
w.disp = nil
w.invMu.Unlock()
}
return evt return evt
} }
} }
@@ -1414,11 +1410,7 @@ func (w *window) Invalidate() {
default: default:
return return
} }
w.invMu.Lock() w.disp.wakeup()
defer w.invMu.Unlock()
if w.disp != nil {
w.disp.wakeup()
}
} }
func (w *window) Run(f func()) { func (w *window) Run(f func()) {
@@ -1513,6 +1505,10 @@ func (d *wlDisplay) wakeup() {
} }
func (w *window) destroy() { func (w *window) destroy() {
if w.lastFrameCallback != nil {
C.wl_callback_destroy(w.lastFrameCallback)
w.lastFrameCallback = nil
}
if w.cursor.surf != nil { if w.cursor.surf != nil {
C.wl_surface_destroy(w.cursor.surf) C.wl_surface_destroy(w.cursor.surf)
} }
@@ -1637,6 +1633,14 @@ func (w *window) flushScroll() {
if total == (f32.Point{}) { if total == (f32.Point{}) {
return return
} }
if w.scroll.steps == (image.Point{}) {
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
}
// Zero scroll distance prior to calling ProcessEvent, otherwise we may recursively
// re-process the scroll distance.
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
w.ProcessEvent(pointer.Event{ w.ProcessEvent(pointer.Event{
Kind: pointer.Scroll, Kind: pointer.Scroll,
Source: pointer.Mouse, Source: pointer.Mouse,
@@ -1646,12 +1650,6 @@ func (w *window) flushScroll() {
Time: w.scroll.time, Time: w.scroll.time,
Modifiers: w.disp.xkb.Modifiers(), Modifiers: w.disp.xkb.Modifiers(),
}) })
if w.scroll.steps == (image.Point{}) {
w.fling.xExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.X)
w.fling.yExtrapolation.SampleDelta(w.scroll.time, -w.scroll.dist.Y)
}
w.scroll.dist = f32.Point{}
w.scroll.steps = image.Point{}
} }
func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) { func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) {
+14 -18
View File
@@ -50,14 +50,10 @@ type window struct {
placement *windows.WindowPlacement placement *windows.WindowPlacement
animating bool animating bool
focused bool
borderSize image.Point borderSize image.Point
config Config config Config
loop *eventLoop loop *eventLoop
// invMu avoids the race between destroying the window and Invalidate.
invMu sync.Mutex
} }
const _WM_WAKEUP = windows.WM_USER + iota const _WM_WAKEUP = windows.WM_USER + iota
@@ -269,11 +265,11 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
Kind: pointer.Cancel, Kind: pointer.Cancel,
}) })
case windows.WM_SETFOCUS: case windows.WM_SETFOCUS:
w.focused = true w.config.Focused = true
w.ProcessEvent(key.FocusEvent{Focus: true}) w.ProcessEvent(ConfigEvent{Config: w.config})
case windows.WM_KILLFOCUS: case windows.WM_KILLFOCUS:
w.focused = false w.config.Focused = false
w.ProcessEvent(key.FocusEvent{Focus: false}) w.ProcessEvent(ConfigEvent{Config: w.config})
case windows.WM_NCHITTEST: case windows.WM_NCHITTEST:
if w.config.Decorated { if w.config.Decorated {
// Let the system handle it. // Let the system handle it.
@@ -305,10 +301,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
windows.ReleaseDC(w.hdc) windows.ReleaseDC(w.hdc)
w.hdc = 0 w.hdc = 0
} }
w.invMu.Lock()
// The system destroys the HWND for us. // The system destroys the HWND for us.
w.hwnd = 0 w.hwnd = 0
w.invMu.Unlock()
windows.PostQuitMessage(0) windows.PostQuitMessage(0)
case windows.WM_NCCALCSIZE: case windows.WM_NCCALCSIZE:
if w.config.Decorated { if w.config.Decorated {
@@ -332,6 +326,12 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
mi := windows.GetMonitorInfo(w.hwnd) mi := windows.GetMonitorInfo(w.hwnd)
szp.Rgrc[0] = mi.WorkArea szp.Rgrc[0] = mi.WorkArea
return 0 return 0
case windows.WM_NCLBUTTONDBLCLK:
if !w.config.Decorated {
// Override Windows behaviour when we
// draw decorations.
return 0
}
case windows.WM_PAINT: case windows.WM_PAINT:
w.draw(true) w.draw(true)
case windows.WM_SIZE: case windows.WM_SIZE:
@@ -496,7 +496,7 @@ func (w *window) hitTest(x, y int) uintptr {
} }
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) { func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
if !w.focused { if !w.config.Focused {
windows.SetFocus(w.hwnd) windows.SetFocus(w.hwnd)
} }
@@ -621,13 +621,6 @@ func (w *window) Frame(frame *op.Ops) {
} }
func (w *window) wakeup() { func (w *window) wakeup() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.hwnd == 0 {
w.loop.Wakeup()
w.loop.FlushEvents()
return
}
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil { if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
panic(err) panic(err)
} }
@@ -994,3 +987,6 @@ func configForDPI(dpi int) unit.Metric {
func (Win32ViewEvent) implementsViewEvent() {} func (Win32ViewEvent) implementsViewEvent() {}
func (Win32ViewEvent) ImplementsEvent() {} func (Win32ViewEvent) ImplementsEvent() {}
func (w Win32ViewEvent) Valid() bool {
return w != (Win32ViewEvent{})
}
+14 -16
View File
@@ -113,9 +113,6 @@ type x11Window struct {
wakeups chan struct{} wakeups chan struct{}
handler x11EventHandler handler x11EventHandler
buf [100]byte buf [100]byte
// invMy avoids the race between destroy and Invalidate.
invMu sync.Mutex
} }
var ( var (
@@ -389,6 +386,7 @@ func (w *x11Window) ProcessEvent(e event.Event) {
func (w *x11Window) shutdown(err error) { func (w *x11Window) shutdown(err error) {
w.ProcessEvent(X11ViewEvent{}) w.ProcessEvent(X11ViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err}) w.ProcessEvent(DestroyEvent{Err: err})
w.destroy()
} }
func (w *x11Window) Event() event.Event { func (w *x11Window) Event() event.Event {
@@ -398,9 +396,6 @@ func (w *x11Window) Event() event.Event {
w.dispatch() w.dispatch()
continue continue
} }
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
}
return evt return evt
} }
} }
@@ -418,11 +413,6 @@ func (w *x11Window) Invalidate() {
case w.wakeups <- struct{}{}: case w.wakeups <- struct{}{}:
default: default:
} }
w.invMu.Lock()
defer w.invMu.Unlock()
if w.x == nil {
return
}
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN { if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err)) panic(fmt.Errorf("failed to write to pipe: %v", err))
} }
@@ -464,7 +454,12 @@ func (w *x11Window) dispatch() {
// Check for pending draw events before checking animation or blocking. // Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but // This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll. // poll will still block. This also prevents no-op calls to poll.
if syn = w.handler.handleEvents(); !syn { syn = w.handler.handleEvents()
if w.x == nil {
// handleEvents received a close request and destroyed the window.
return
}
if !syn {
anim = w.animating anim = w.animating
if !anim { if !anim {
// Clear poll events. // Clear poll events.
@@ -476,6 +471,9 @@ func (w *x11Window) dispatch() {
switch { switch {
case *xEvents&syscall.POLLIN != 0: case *xEvents&syscall.POLLIN != 0:
syn = w.handler.handleEvents() syn = w.handler.handleEvents()
if w.x == nil {
return
}
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0: case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
} }
} }
@@ -503,8 +501,6 @@ func (w *x11Window) dispatch() {
} }
func (w *x11Window) destroy() { func (w *x11Window) destroy() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.notify.write != 0 { if w.notify.write != 0 {
syscall.Close(w.notify.write) syscall.Close(w.notify.write)
w.notify.write = 0 w.notify.write = 0
@@ -657,9 +653,11 @@ func (h *x11EventHandler) handleEvents() bool {
// redraw only on the last expose event // redraw only on the last expose event
redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0 redraw = (*C.XExposeEvent)(unsafe.Pointer(xev)).count == 0
case C.FocusIn: case C.FocusIn:
w.ProcessEvent(key.FocusEvent{Focus: true}) w.config.Focused = true
w.ProcessEvent(ConfigEvent{Config: w.config})
case C.FocusOut: case C.FocusOut:
w.ProcessEvent(key.FocusEvent{Focus: false}) w.config.Focused = false
w.ProcessEvent(ConfigEvent{Config: w.config})
case C.ConfigureNotify: // window configuration change case C.ConfigureNotify: // window configuration change
cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev)) cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev))
if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size { if sz := image.Pt(int(cevt.width), int(cevt.height)); sz != w.config.Size {
+129 -72
View File
@@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
"reflect"
"runtime" "runtime"
"sync" "sync"
"time" "time"
@@ -36,14 +35,15 @@ type Option func(unit.Metric, *Config)
// Window represents an operating system window. // Window represents an operating system window.
// //
// The zero-value Window is useful, and calling any method on // The zero-value Window is useful; the GUI window is created and shown the first
// it creates and shows a new GUI window. On iOS or Android, // time the [Event] method is called. On iOS or Android, the first Window represents
// the first Window represents the the window previously // the window previously created by the platform.
// created by the platform.
// //
// More than one Window is not supported on iOS, Android, // More than one Window is not supported on iOS, Android, WebAssembly.
// WebAssembly.
type Window struct { type Window struct {
initialOpts []Option
initialActions []system.Action
ctx context ctx context
gpu gpu.GPU gpu gpu.GPU
// timer tracks the delayed invalidate goroutine. // timer tracks the delayed invalidate goroutine.
@@ -89,13 +89,15 @@ type Window struct {
} }
imeState editorState imeState editorState
driver driver driver driver
// basic is the driver interface that is needed even after the window is gone.
basic basicDriver // invMu protects mayInvalidate.
once sync.Once invMu sync.Mutex
mayInvalidate bool
// coalesced tracks the most recent events waiting to be delivered // coalesced tracks the most recent events waiting to be delivered
// to the client. // to the client.
coalesced eventSummary coalesced eventSummary
// frame tracks the most recently frame event. // frame tracks the most recent frame event.
lastFrame struct { lastFrame struct {
sync bool sync bool
size image.Point size image.Point
@@ -105,11 +107,12 @@ type Window struct {
} }
type eventSummary struct { type eventSummary struct {
wakeup bool wakeup bool
cfg *ConfigEvent cfg *ConfigEvent
view *ViewEvent view *ViewEvent
frame *frameEvent frame *frameEvent
destroy *DestroyEvent framePending bool
destroy *DestroyEvent
} }
type callbacks struct { type callbacks struct {
@@ -216,6 +219,7 @@ func (w *Window) frame(frame *op.Ops, viewport image.Point) error {
} }
func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) { func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
w.coalesced.framePending = false
wrapper := &w.decorations.Ops wrapper := &w.decorations.Ops
off := op.Offset(w.lastFrame.off).Push(wrapper) off := op.Offset(w.lastFrame.off).Push(wrapper)
ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal)) ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal))
@@ -273,8 +277,12 @@ func (w *Window) updateState() {
// //
// Invalidate is safe for concurrent use. // Invalidate is safe for concurrent use.
func (w *Window) Invalidate() { func (w *Window) Invalidate() {
w.init() w.invMu.Lock()
w.basic.Invalidate() defer w.invMu.Unlock()
if w.mayInvalidate {
w.mayInvalidate = false
w.driver.Invalidate()
}
} }
// Option applies the options to the window. The options are hints; the platform is // Option applies the options to the window. The options are hints; the platform is
@@ -283,7 +291,10 @@ func (w *Window) Option(opts ...Option) {
if len(opts) == 0 { if len(opts) == 0 {
return return
} }
w.init(opts...) if w.driver == nil {
w.initialOpts = append(w.initialOpts, opts...)
return
}
w.Run(func() { w.Run(func() {
cnf := Config{Decorated: w.decorations.enabled} cnf := Config{Decorated: w.decorations.enabled}
for _, opt := range opts { for _, opt := range opts {
@@ -302,16 +313,14 @@ func (w *Window) Option(opts ...Option) {
} }
// Run f in the same thread as the native window event loop, and wait for f to // Run f in the same thread as the native window event loop, and wait for f to
// return or the window to close. Run is guaranteed not to deadlock if it is // return or the window to close. If the window has not yet been created,
// invoked during the handling of a [ViewEvent], [FrameEvent], // Run calls f directly.
// [StageEvent]; call Run in a separate goroutine to avoid deadlock in all
// other cases.
// //
// Note that most programs should not call Run; configuring a Window with // Note that most programs should not call Run; configuring a Window with
// [CustomRenderer] is a notable exception. // [CustomRenderer] is a notable exception.
func (w *Window) Run(f func()) { func (w *Window) Run(f func()) {
w.init()
if w.driver == nil { if w.driver == nil {
f()
return return
} }
done := make(chan struct{}) done := make(chan struct{})
@@ -377,11 +386,11 @@ func (w *Window) setNextFrame(at time.Time) {
} }
} }
func (c *callbacks) SetDriver(d basicDriver) { func (c *callbacks) SetDriver(d driver) {
c.w.basic = d if d == nil {
if d, ok := d.(driver); ok { panic("nil driver")
c.w.driver = d
} }
c.w.driver = d
} }
func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) { func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) {
@@ -548,10 +557,20 @@ func (c *callbacks) Invalidate() {
} }
func (c *callbacks) nextEvent() (event.Event, bool) { func (c *callbacks) nextEvent() (event.Event, bool) {
s := &c.w.coalesced return c.w.nextEvent()
// Every event counts as a wakeup. }
defer func() { s.wakeup = false }()
func (w *Window) nextEvent() (event.Event, bool) {
s := &w.coalesced
defer func() {
// Every event counts as a wakeup.
s.wakeup = false
}()
switch { switch {
case s.framePending:
// If the user didn't call FrameEvent.Event, process
// an empty frame.
w.processFrame(new(op.Ops), nil)
case s.view != nil: case s.view != nil:
e := *s.view e := *s.view
s.view = nil s.view = nil
@@ -568,10 +587,14 @@ func (c *callbacks) nextEvent() (event.Event, bool) {
case s.frame != nil: case s.frame != nil:
e := *s.frame e := *s.frame
s.frame = nil s.frame = nil
s.framePending = true
return e.FrameEvent, true return e.FrameEvent, true
case s.wakeup: case s.wakeup:
return wakeupEvent{}, true return wakeupEvent{}, true
} }
w.invMu.Lock()
defer w.invMu.Unlock()
w.mayInvalidate = w.driver != nil
return nil, false return nil, false
} }
@@ -615,6 +638,9 @@ func (w *Window) processEvent(e event.Event) bool {
w.coalesced.frame = &e2 w.coalesced.frame = &e2
case DestroyEvent: case DestroyEvent:
w.destroyGPU() w.destroyGPU()
w.invMu.Lock()
w.mayInvalidate = false
w.invMu.Unlock()
w.driver = nil w.driver = nil
if q := w.timer.quit; q != nil { if q := w.timer.quit; q != nil {
q <- struct{}{} q <- struct{}{}
@@ -622,7 +648,7 @@ func (w *Window) processEvent(e event.Event) bool {
} }
w.coalesced.destroy = &e2 w.coalesced.destroy = &e2
case ViewEvent: case ViewEvent:
if reflect.ValueOf(e2).IsZero() && w.gpu != nil { if !e2.Valid() && w.gpu != nil {
w.ctx.Lock() w.ctx.Lock()
w.gpu.Release() w.gpu.Release()
w.gpu = nil w.gpu = nil
@@ -630,9 +656,19 @@ func (w *Window) processEvent(e event.Event) bool {
} }
w.coalesced.view = &e2 w.coalesced.view = &e2
case ConfigEvent: case ConfigEvent:
wasFocused := w.decorations.Config.Focused
w.decorations.Config = e2.Config w.decorations.Config = e2.Config
e2.Config = w.effectiveConfig() e2.Config = w.effectiveConfig()
w.coalesced.cfg = &e2 w.coalesced.cfg = &e2
if f := w.decorations.Config.Focused; f != wasFocused {
w.queue.Queue(key.FocusEvent{Focus: f})
}
t, handled := w.queue.WakeupTime()
if handled {
w.setNextFrame(t)
w.updateAnimation()
}
return handled
case event.Event: case event.Event:
focusDir := key.FocusDirection(-1) focusDir := key.FocusDirection(-1)
if e, ok := e2.(key.Event); ok && e.State == key.Press { if e, ok := e2.(key.Event); ok && e.State == key.Press {
@@ -673,48 +709,61 @@ func (w *Window) processEvent(e event.Event) bool {
} }
// Event blocks until an event is received from the window, such as // Event blocks until an event is received from the window, such as
// [FrameEvent], or until [Invalidate] is called. // [FrameEvent], or until [Invalidate] is called. The window is created
// and shown the first time Event is called.
func (w *Window) Event() event.Event { func (w *Window) Event() event.Event {
w.init() if w.driver == nil {
return w.basic.Event() w.init()
}
if w.driver == nil {
e, ok := w.nextEvent()
if !ok {
panic("window initializion failed without a DestroyEvent")
}
return e
}
return w.driver.Event()
} }
func (w *Window) init(initial ...Option) { func (w *Window) init() {
w.once.Do(func() { debug.Parse()
debug.Parse() // Measure decoration height.
// Measure decoration height. deco := new(widget.Decorations)
deco := new(widget.Decorations) theme := material.NewTheme()
theme := material.NewTheme() theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular())) decoStyle := material.Decorations(theme, deco, 0, "")
decoStyle := material.Decorations(theme, deco, 0, "") gtx := layout.Context{
gtx := layout.Context{ Ops: new(op.Ops),
Ops: new(op.Ops), // Measure in Dp.
// Measure in Dp. Metric: unit.Metric{},
Metric: unit.Metric{}, }
} // Allow plenty of space.
// Allow plenty of space. gtx.Constraints.Max.Y = 200
gtx.Constraints.Max.Y = 200 dims := decoStyle.Layout(gtx)
dims := decoStyle.Layout(gtx) decoHeight := unit.Dp(dims.Size.Y)
decoHeight := unit.Dp(dims.Size.Y) defaultOptions := []Option{
defaultOptions := []Option{ Size(800, 600),
Size(800, 600), Title("Gio"),
Title("Gio"), Decorated(true),
Decorated(true), decoHeightOpt(decoHeight),
decoHeightOpt(decoHeight), }
} options := append(defaultOptions, w.initialOpts...)
options := append(defaultOptions, initial...) w.initialOpts = nil
var cnf Config var cnf Config
cnf.apply(unit.Metric{}, options) cnf.apply(unit.Metric{}, options)
w.nocontext = cnf.CustomRenderer w.nocontext = cnf.CustomRenderer
w.decorations.Theme = theme w.decorations.Theme = theme
w.decorations.Decorations = deco w.decorations.Decorations = deco
w.decorations.enabled = cnf.Decorated w.decorations.enabled = cnf.Decorated
w.decorations.height = decoHeight w.decorations.height = decoHeight
w.imeState.compose = key.Range{Start: -1, End: -1} w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[input.SemanticID]input.SemanticNode) w.semantic.ids = make(map[input.SemanticID]input.SemanticNode)
newWindow(&callbacks{w}, options) newWindow(&callbacks{w}, options)
}) for _, acts := range w.initialActions {
w.Perform(acts)
}
w.initialActions = nil
} }
func (w *Window) updateCursor() { func (w *Window) updateCursor() {
@@ -762,8 +811,12 @@ func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point {
} }
// Update the window based on the actions on the decorations. // Update the window based on the actions on the decorations.
opts, acts := splitActions(deco.Update(gtx)) opts, acts := splitActions(deco.Update(gtx))
w.driver.Configure(opts) if len(opts) > 0 {
w.driver.Perform(acts) w.driver.Configure(opts)
}
if acts != 0 {
w.driver.Perform(acts)
}
style.Layout(gtx) style.Layout(gtx)
// Offset to place the frame content below the decorations. // Offset to place the frame content below the decorations.
decoHeight := gtx.Dp(w.decorations.Config.decoHeight) decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
@@ -810,6 +863,10 @@ func (w *Window) Perform(actions system.Action) {
if acts == 0 { if acts == 0 {
return return
} }
if w.driver == nil {
w.initialActions = append(w.initialActions, acts)
return
}
w.Run(func() { w.Run(func() {
w.driver.Perform(actions) w.driver.Perform(actions)
}) })
+5 -4
View File
@@ -271,12 +271,13 @@ func (s *Scroll) Stop() {
} }
// Update state and report the scroll distance along axis. // Update state and report the scroll distance along axis.
func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, bounds image.Rectangle) int { func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, scrollx, scrolly pointer.ScrollRange) int {
total := 0 total := 0
f := pointer.Filter{ f := pointer.Filter{
Target: s, Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel, Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollBounds: bounds, ScrollX: scrollx,
ScrollY: scrolly,
} }
for { for {
evt, ok := q.Event(f) evt, ok := q.Event(f)
+4 -4
View File
@@ -1,16 +1,16 @@
module gioui.org module gioui.org
go 1.19 go 1.21
require ( require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.8 gioui.org/shader v1.0.8
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 github.com/go-text/typesetting v0.1.1
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
golang.org/x/image v0.5.0 golang.org/x/image v0.5.0
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 golang.org/x/sys v0.5.0
) )
require golang.org/x/text v0.7.0 require golang.org/x/text v0.9.0
+8 -6
View File
@@ -5,9 +5,10 @@ gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJG
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo= github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k= github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI= github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -28,15 +29,16 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+99 -89
View File
@@ -261,7 +261,7 @@ type texture struct {
type blitter struct { type blitter struct {
ctx driver.Device ctx driver.Device
viewport image.Point viewport image.Point
pipelines [3]*pipeline pipelines [2][3]*pipeline
colUniforms *blitColUniforms colUniforms *blitColUniforms
texUniforms *blitTexUniforms texUniforms *blitTexUniforms
linearGradientUniforms *blitLinearGradientUniforms linearGradientUniforms *blitLinearGradientUniforms
@@ -560,12 +560,24 @@ func newBlitter(ctx driver.Device) *blitter {
func (b *blitter) release() { func (b *blitter) release() {
b.quadVerts.Release() b.quadVerts.Release()
for _, p := range b.pipelines { for _, p := range b.pipelines {
p.Release() for _, p := range p {
p.Release()
}
} }
} }
func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) ([3]*pipeline, error) { func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) (pipelines [2][3]*pipeline, err error) {
var pipelines [3]*pipeline defer func() {
if err != nil {
for _, p := range pipelines {
for _, p := range p {
if p != nil {
p.Release()
}
}
}
}
}()
blend := driver.BlendDesc{ blend := driver.BlendDesc{
Enable: true, Enable: true,
SrcFactor: driver.BlendFactorOne, SrcFactor: driver.BlendFactorOne,
@@ -583,86 +595,76 @@ func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.
return pipelines, err return pipelines, err
} }
defer vsh.Release() defer vsh.Release()
{ for i, format := range []driver.TextureFormat{driver.TextureFormatOutput, driver.TextureFormatSRGBA} {
fsh, err := b.NewFragmentShader(fsSrc[materialTexture]) {
if err != nil { fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
return pipelines, err if err != nil {
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: format,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
return pipelines, err
}
var vertBuffer *uniformBuffer
if u := uniforms[materialTexture]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[i][materialTexture] = &pipeline{pipe, vertBuffer}
} }
defer fsh.Release() {
pipe, err := b.NewPipeline(driver.PipelineDesc{ var vertBuffer *uniformBuffer
VertexShader: vsh, fsh, err := b.NewFragmentShader(fsSrc[materialColor])
FragmentShader: fsh, if err != nil {
BlendDesc: blend, return pipelines, err
VertexLayout: layout, }
PixelFormat: driver.TextureFormatOutput, defer fsh.Release()
Topology: driver.TopologyTriangleStrip, pipe, err := b.NewPipeline(driver.PipelineDesc{
}) VertexShader: vsh,
if err != nil { FragmentShader: fsh,
return pipelines, err BlendDesc: blend,
VertexLayout: layout,
PixelFormat: format,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
return pipelines, err
}
if u := uniforms[materialColor]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[i][materialColor] = &pipeline{pipe, vertBuffer}
} }
var vertBuffer *uniformBuffer {
if u := uniforms[materialTexture]; u != nil { var vertBuffer *uniformBuffer
vertBuffer = newUniformBuffer(b, u) fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient])
if err != nil {
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: format,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
return pipelines, err
}
if u := uniforms[materialLinearGradient]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[i][materialLinearGradient] = &pipeline{pipe, vertBuffer}
} }
pipelines[materialTexture] = &pipeline{pipe, vertBuffer}
}
{
var vertBuffer *uniformBuffer
fsh, err := b.NewFragmentShader(fsSrc[materialColor])
if err != nil {
pipelines[materialTexture].Release()
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: driver.TextureFormatOutput,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
pipelines[materialTexture].Release()
return pipelines, err
}
if u := uniforms[materialColor]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[materialColor] = &pipeline{pipe, vertBuffer}
}
{
var vertBuffer *uniformBuffer
fsh, err := b.NewFragmentShader(fsSrc[materialLinearGradient])
if err != nil {
pipelines[materialTexture].Release()
pipelines[materialColor].Release()
return pipelines, err
}
defer fsh.Release()
pipe, err := b.NewPipeline(driver.PipelineDesc{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: driver.TextureFormatOutput,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
pipelines[materialTexture].Release()
pipelines[materialColor].Release()
return pipelines, err
}
if u := uniforms[materialLinearGradient]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[materialLinearGradient] = &pipeline{pipe, vertBuffer}
}
if err != nil {
for _, p := range pipelines {
p.Release()
}
return pipelines, err
} }
return pipelines, nil return pipelines, nil
} }
@@ -865,7 +867,7 @@ func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
Min: l.place.Pos, Min: l.place.Pos,
Max: l.place.Pos.Add(l.clip.Size()), Max: l.place.Pos.Add(l.clip.Size()),
} }
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y) r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy())
f := r.layerFBOs.fbos[fbo] f := r.layerFBOs.fbos[fbo]
r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd]) r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
sr := f32.FRect(v) sr := f32.FRect(v)
@@ -930,7 +932,7 @@ func (d *drawOps) newPathOp() *pathOp {
return &d.pathOpCache[len(d.pathOpCache)-1] return &d.pathOpCache[len(d.pathOpCache)-1]
} }
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point, push bool) { func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point) {
npath := d.newPathOp() npath := d.newPathOp()
*npath = pathOp{ *npath = pathOp{
parent: state.cpath, parent: state.cpath,
@@ -1055,7 +1057,7 @@ loop:
quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans) quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans)
quads.key = opKey{Key: encOp.Key} quads.key = opKey{Key: encOp.Key}
} }
d.addClipPath(&state, quads.aux, quads.key, bounds, off, true) d.addClipPath(&state, quads.aux, quads.key, bounds, off)
quads = quadsOp{} quads = quadsOp{}
case ops.TypePopClip: case ops.TypePopClip:
state.cpath = state.cpath.parent state.cpath = state.cpath.parent
@@ -1100,7 +1102,7 @@ loop:
// this transformed rectangle. // this transformed rectangle.
k := opKey{Key: encOp.Key} k := opKey{Key: encOp.Key}
k.SetTransform(t) // TODO: This call has no effect. k.SetTransform(t) // TODO: This call has no effect.
d.addClipPath(&state, clipData, k, bnd, off, false) d.addClipPath(&state, clipData, k, bnd, off)
} }
bounds := cl.Round() bounds := cl.Round()
@@ -1230,7 +1232,7 @@ func (r *renderer) prepareDrawOps(ops []imageOp) {
} }
} }
func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point, ops []imageOp) { func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp) {
var coverTex driver.Texture var coverTex driver.Texture
for i := 0; i < len(ops); i++ { for i := 0; i < len(ops); i++ {
img := ops[i] img := ops[i]
@@ -1244,9 +1246,13 @@ func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point,
scale, off := clipSpaceTransform(drc, viewport) scale, off := clipSpaceTransform(drc, viewport)
var fbo FBO var fbo FBO
fboIdx := 0
if isFBO {
fboIdx = 1
}
switch img.clipType { switch img.clipType {
case clipTypeNone: case clipTypeNone:
p := r.blitter.pipelines[m.material] p := r.blitter.pipelines[fboIdx][m.material]
r.ctx.BindPipeline(p.pipeline) r.ctx.BindPipeline(p.pipeline)
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0) r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans) r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans)
@@ -1265,7 +1271,7 @@ func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point,
Max: img.place.Pos.Add(drc.Size()), Max: img.place.Pos.Add(drc.Size()),
} }
coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size) coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size)
p := r.pather.coverer.pipelines[m.material] p := r.pather.coverer.pipelines[fboIdx][m.material]
r.ctx.BindPipeline(p.pipeline) r.ctx.BindPipeline(p.pipeline)
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0) r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff) r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
@@ -1273,7 +1279,11 @@ func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point,
} }
func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) { func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) {
p := b.pipelines[mat] fboIdx := 0
if fbo {
fboIdx = 1
}
p := b.pipelines[fboIdx][mat]
b.ctx.BindPipeline(p.pipeline) b.ctx.BindPipeline(p.pipeline)
var uniforms *blitUniforms var uniforms *blitUniforms
switch mat { switch mat {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 B

After

Width:  |  Height:  |  Size: 1.9 KiB

+2 -2
View File
@@ -483,14 +483,14 @@ func TestGapsInPath(t *testing.T) {
func TestOpacity(t *testing.T) { func TestOpacity(t *testing.T) {
run(t, func(ops *op.Ops) { run(t, func(ops *op.Ops) {
opc1 := paint.PushOpacity(ops, .3) opc1 := paint.PushOpacity(ops, .3)
// Fill screen to exercize the glClear optimization. // Fill screen to exercise the glClear optimization.
paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op()) paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op())
opc2 := paint.PushOpacity(ops, .6) opc2 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op()) paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op())
opc2.Pop() opc2.Pop()
opc1.Pop() opc1.Pop()
opc3 := paint.PushOpacity(ops, .6) opc3 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(50+20, 10), Max: image.Pt(50+64, 128)}.Op()) paint.FillShape(ops, color.NRGBA{B: 255, A: 255}, clip.Ellipse(image.Rectangle{Min: image.Pt(20+20, 10), Max: image.Pt(50+64, 128)}).Op(ops))
opc3.Pop() opc3.Pop()
}, func(r result) { }, func(r result) {
}) })
+9 -3
View File
@@ -30,7 +30,7 @@ type pather struct {
type coverer struct { type coverer struct {
ctx driver.Device ctx driver.Device
pipelines [3]*pipeline pipelines [2][3]*pipeline
texUniforms *coverTexUniforms texUniforms *coverTexUniforms
colUniforms *coverColUniforms colUniforms *coverColUniforms
linearGradientUniforms *coverLinearGradientUniforms linearGradientUniforms *coverLinearGradientUniforms
@@ -309,7 +309,9 @@ func (p *pather) release() {
func (c *coverer) release() { func (c *coverer) release() {
for _, p := range c.pipelines { for _, p := range c.pipelines {
p.Release() for _, p := range p {
p.Release()
}
} }
} }
@@ -405,7 +407,11 @@ func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, c
} }
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y} uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y} uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
c.pipelines[mat].UploadUniforms(c.ctx) fboIdx := 0
if isFBO {
fboIdx = 1
}
c.pipelines[fboIdx][mat].UploadUniforms(c.ctx)
c.ctx.DrawArrays(0, 4) c.ctx.DrawArrays(0, 4)
} }
+4 -7
View File
@@ -15,10 +15,9 @@ import (
) )
type Context struct { type Context struct {
disp _EGLDisplay disp _EGLDisplay
eglCtx *eglContext eglCtx *eglContext
eglSurf _EGLSurface eglSurf _EGLSurface
width, height int
} }
type eglContext struct { type eglContext struct {
@@ -121,11 +120,9 @@ func (c *Context) VisualID() int {
return c.eglCtx.visualID return c.eglCtx.visualID
} }
func (c *Context) CreateSurface(win NativeWindowType, width, height int) error { func (c *Context) CreateSurface(win NativeWindowType) error {
eglSurf, err := createSurface(c.disp, c.eglCtx, win) eglSurf, err := createSurface(c.disp, c.eglCtx, win)
c.eglSurf = eglSurf c.eglSurf = eglSurf
c.width = width
c.height = height
return err return err
} }
+3
View File
@@ -361,6 +361,9 @@ func (c *Functions) GetProgrami(p Program, pname Enum) int {
} }
func (c *Functions) GetProgramInfoLog(p Program) string { func (c *Functions) GetProgramInfoLog(p Program) string {
n := c.GetProgrami(p, INFO_LOG_LENGTH) n := c.GetProgrami(p, INFO_LOG_LENGTH)
if n == 0 {
return ""
}
buf := make([]byte, n) buf := make([]byte, n)
syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0) syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0)
return string(buf) return string(buf)
+3
View File
@@ -327,6 +327,9 @@ func strokePathNorm(p0, p1, p2 f32.Point, t, d float32) f32.Point {
func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) } func rot90CW(p f32.Point) f32.Point { return f32.Pt(+p.Y, -p.X) }
func normPt(p f32.Point, l float32) f32.Point { func normPt(p f32.Point, l float32) f32.Point {
if (p.X == 0 && p.Y == l) || (p.Y == 0 && p.X == l) {
return f32.Point{X: p.X, Y: p.Y}
}
d := math.Hypot(float64(p.X), float64(p.Y)) d := math.Hypot(float64(p.X), float64(p.Y))
l64 := float64(l) l64 := float64(l)
if math.Abs(d-l64) < 1e-10 { if math.Abs(d-l64) < 1e-10 {
+4 -3
View File
@@ -251,9 +251,10 @@ func TestFocusScroll(t *testing.T) {
filters := []event.Filter{ filters := []event.Filter{
key.FocusFilter{Target: h}, key.FocusFilter{Target: h},
pointer.Filter{ pointer.Filter{
Target: h, Target: h,
Kinds: pointer.Scroll, Kinds: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100), ScrollX: pointer.ScrollRange{Min: -100, Max: +100},
ScrollY: pointer.ScrollRange{Min: -100, Max: +100},
}, },
} }
events(r, -1, filters...) events(r, -1, filters...)
+7 -5
View File
@@ -72,7 +72,7 @@ type pointerHandler struct {
type pointerFilter struct { type pointerFilter struct {
kinds pointer.Kind kinds pointer.Kind
// min and max horizontal/vertical scroll // min and max horizontal/vertical scroll
scrollRange image.Rectangle scrollX, scrollY pointer.ScrollRange
sourceMimes []string sourceMimes []string
targetMimes []string targetMimes []string
@@ -297,7 +297,8 @@ func (p *pointerFilter) Add(f event.Filter) {
p.targetMimes = append(p.targetMimes, f.Type) p.targetMimes = append(p.targetMimes, f.Type)
case pointer.Filter: case pointer.Filter:
p.kinds = p.kinds | f.Kinds p.kinds = p.kinds | f.Kinds
p.scrollRange = p.scrollRange.Union(f.ScrollBounds) p.scrollX = p.scrollX.Union(f.ScrollX)
p.scrollY = p.scrollY.Union(f.ScrollY)
} }
} }
@@ -325,7 +326,8 @@ func (p *pointerFilter) Matches(e event.Event) bool {
func (p *pointerFilter) Merge(p2 pointerFilter) { func (p *pointerFilter) Merge(p2 pointerFilter) {
p.kinds = p.kinds | p2.kinds p.kinds = p.kinds | p2.kinds
p.scrollRange = p.scrollRange.Union(p2.scrollRange) p.scrollX = p.scrollX.Union(p2.scrollX)
p.scrollY = p.scrollY.Union(p2.scrollY)
p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...) p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...)
p.targetMimes = append(p.targetMimes, p2.targetMimes...) p.targetMimes = append(p.targetMimes, p2.targetMimes...)
} }
@@ -333,8 +335,8 @@ func (p *pointerFilter) Merge(p2 pointerFilter) {
// clampScroll splits a scroll distance in the remaining scroll and the // clampScroll splits a scroll distance in the remaining scroll and the
// scroll accepted by the filter. // scroll accepted by the filter.
func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) { func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) {
left.X, scrolled.X = clampSplit(scroll.X, p.scrollRange.Min.X, p.scrollRange.Max.X) left.X, scrolled.X = clampSplit(scroll.X, p.scrollX.Min, p.scrollX.Max)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollRange.Min.Y, p.scrollRange.Max.Y) left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max)
return return
} }
+37 -9
View File
@@ -300,9 +300,9 @@ func TestPointerPriority(t *testing.T) {
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
f1 := func(t event.Tag) event.Filter { f1 := func(t event.Tag) event.Filter {
return pointer.Filter{ return pointer.Filter{
Target: t, Target: t,
Kinds: pointer.Scroll, Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 100}}, ScrollX: pointer.ScrollRange{Max: 100},
} }
} }
events(&r, -1, f1(handler1)) events(&r, -1, f1(handler1))
@@ -311,9 +311,9 @@ func TestPointerPriority(t *testing.T) {
r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops) r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops)
f2 := func(t event.Tag) event.Filter { f2 := func(t event.Tag) event.Filter {
return pointer.Filter{ return pointer.Filter{
Target: t, Target: t,
Kinds: pointer.Scroll, Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}}, ScrollX: pointer.ScrollRange{Max: 20},
} }
} }
events(&r, -1, f2(handler2)) events(&r, -1, f2(handler2))
@@ -324,9 +324,10 @@ func TestPointerPriority(t *testing.T) {
r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops) r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops)
f3 := func(t event.Tag) event.Filter { f3 := func(t event.Tag) event.Filter {
return pointer.Filter{ return pointer.Filter{
Target: t, Target: t,
Kinds: pointer.Scroll, Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}}, ScrollX: pointer.ScrollRange{Min: -20},
ScrollY: pointer.ScrollRange{Min: -40},
} }
} }
events(&r, -1, f3(handler3)) events(&r, -1, f3(handler3))
@@ -1085,6 +1086,33 @@ func TestPassCursor(t *testing.T) {
} }
} }
func TestPartialEvent(t *testing.T) {
var ops op.Ops
var r Router
rect := clip.Rect(image.Rect(0, 0, 100, 100))
background := rect.Push(&ops)
event.Op(&ops, 1)
background.Pop()
overlayPass := pointer.PassOp{}.Push(&ops)
overlay := rect.Push(&ops)
event.Op(&ops, 2)
overlay.Pop()
overlayPass.Pop()
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 1, Kinds: pointer.Press}))
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 2, Kinds: pointer.Press}))
r.Frame(&ops)
r.Queue(pointer.Event{
Kind: pointer.Press,
})
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 1, Kinds: pointer.Press}, key.FocusFilter{Target: 1}),
key.FocusEvent{}, pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Priority: pointer.Shared})
r.Source().Execute(key.FocusCmd{Tag: 1})
assertEventSequence(t, events(&r, -1, pointer.Filter{Target: 2, Kinds: pointer.Press}),
pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Priority: pointer.Foremost})
}
// offer satisfies io.ReadCloser for use in data transfers. // offer satisfies io.ReadCloser for use in data transfers.
type offer struct { type offer struct {
data string data string
+29 -30
View File
@@ -35,10 +35,9 @@ type Router struct {
queue keyQueue queue keyQueue
// The following fields have the same purpose as the fields in // The following fields have the same purpose as the fields in
// type handler, but for key.Events. // type handler, but for key.Events.
filter keyFilter filter keyFilter
nextFilter keyFilter nextFilter keyFilter
processedFilter keyFilter scratchFilter keyFilter
scratchFilter keyFilter
} }
cqueue clipboardQueue cqueue clipboardQueue
// states is the list of pending state changes resulting from // states is the list of pending state changes resulting from
@@ -275,28 +274,29 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
} }
} }
} }
if !q.deferring { for i := range q.changes {
for i := range q.changes { if q.deferring && i > 0 {
change := &q.changes[i] break
for j, evt := range change.events { }
match := false change := &q.changes[i]
switch e := evt.event.(type) { for j, evt := range change.events {
case key.Event: match := false
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false) switch e := evt.event.(type) {
default: case key.Event:
for _, tf := range q.scratchFilters { match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
if evt.tag == tf.tag && tf.filter.Matches(evt.event) { default:
match = true for _, tf := range q.scratchFilters {
break if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
} match = true
break
} }
} }
if match { }
change.events = append(change.events[:j], change.events[j+1:]...) if match {
// Fast forward state to last matched. change.events = append(change.events[:j], change.events[j+1:]...)
q.collapseState(i) // Fast forward state to last matched.
return evt.event, true q.collapseState(i)
} return evt.event, true
} }
} }
} }
@@ -304,7 +304,6 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
h := q.stateFor(tf.tag) h := q.stateFor(tf.tag)
h.processedFilter.Merge(tf.filter) h.processedFilter.Merge(tf.filter)
} }
q.key.processedFilter = append(q.key.processedFilter, q.key.scratchFilter...)
return nil, false return nil, false
} }
@@ -315,15 +314,15 @@ func (q *Router) collapseState(idx int) {
} }
first := &q.changes[0] first := &q.changes[0]
first.state = q.changes[idx].state first.state = q.changes[idx].state
for i := 1; i <= idx; i++ { for _, ch := range q.changes[1 : idx+1] {
first.events = append(first.events, q.changes[i].events...) first.events = append(first.events, ch.events...)
} }
q.changes = append(q.changes[:1], q.changes[idx+1:]...) q.changes = append(q.changes[:1], q.changes[idx+1:]...)
} }
// Frame replaces the declared handlers from the supplied // Frame completes the current frame and starts a new with the
// operation list. The text input state, wakeup time and whether // handlers from the frame argument. Remaining events are discarded,
// there are active profile handlers is also saved. // unless they were deferred by a command.
func (q *Router) Frame(frame *op.Ops) { func (q *Router) Frame(frame *op.Ops) {
var remaining []event.Event var remaining []event.Event
if n := len(q.changes); n > 0 { if n := len(q.changes); n > 0 {
+2 -2
View File
@@ -37,11 +37,11 @@ For example:
var h1, h2 *Handler var h1, h2 *Handler
area := clip.Rect(...).Push(ops) area := clip.Rect(...).Push(ops)
event.Op{Tag: h1}.Add(Ops) event.Op(Ops, h1)
area.Pop() area.Pop()
area := clip.Rect(...).Push(ops) area := clip.Rect(...).Push(ops)
event.Op{Tag: h2}.Add(ops) event.Op(Ops, h2)
area.Pop() area.Pop()
implies a tree of two inner nodes, each with one pointer handler attached. implies a tree of two inner nodes, each with one pointer handler attached.
+19 -6
View File
@@ -3,7 +3,6 @@
package pointer package pointer
import ( import (
"image"
"strings" "strings"
"time" "time"
@@ -61,12 +60,19 @@ type Filter struct {
Target event.Tag Target event.Tag
// Kinds is a bitwise-or of event types to match. // Kinds is a bitwise-or of event types to match.
Kinds Kind Kinds Kind
// ScrollBounds describe the maximum scrollable distances in both // ScrollX and ScrollY constrain the range of scrolling events delivered
// axes. Specifically, any Event e delivered to Tag will satisfy // to Target. Specifically, any Event e delivered to Tag will satisfy
// //
// ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis) // ScrollX.Min <= e.Scroll.X <= ScrollX.Max (horizontal axis)
// ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis) // ScrollY.Min <= e.Scroll.Y <= ScrollY.Max (vertical axis)
ScrollBounds image.Rectangle ScrollX ScrollRange
ScrollY ScrollRange
}
// ScrollRange describes the range of scrolling distances in an
// axis.
type ScrollRange struct {
Min, Max int
} }
// GrabCmd requests a pointer grab on the pointer identified by ID. // GrabCmd requests a pointer grab on the pointer identified by ID.
@@ -219,6 +225,13 @@ const (
ButtonTertiary ButtonTertiary
) )
func (s ScrollRange) Union(s2 ScrollRange) ScrollRange {
return ScrollRange{
Min: min(s.Min, s2.Min),
Max: max(s.Max, s2.Max),
}
}
// Push the current pass mode to the pass stack and set the pass mode. // Push the current pass mode to the pass stack and set the pass mode.
func (p PassOp) Push(o *op.Ops) PassStack { func (p PassOp) Push(o *op.Ops) PassStack {
id, mid := ops.PushOp(&o.Internal, ops.PassStack) id, mid := ops.PushOp(&o.Internal, ops.PassStack)
+6 -4
View File
@@ -7,6 +7,7 @@ import (
"math" "math"
"gioui.org/gesture" "gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
) )
@@ -158,11 +159,12 @@ func (l *List) update(gtx Context) {
max = 0 max = 0
} }
} }
scrollRange := image.Rectangle{ xrange := pointer.ScrollRange{Min: min, Max: max}
Min: l.Axis.Convert(image.Pt(min, 0)), yrange := pointer.ScrollRange{}
Max: l.Axis.Convert(image.Pt(max, 0)), if l.Axis == Vertical {
xrange, yrange = yrange, xrange
} }
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange) d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), xrange, yrange)
l.scrollDelta = d l.scrollDelta = d
l.Position.Offset += d l.Position.Offset += d
} }
+3
View File
@@ -138,6 +138,9 @@ type Path struct {
func (p *Path) Pos() f32.Point { return p.pen } func (p *Path) Pos() f32.Point { return p.pen }
// Begin the path, storing the path data and final Op into ops. // Begin the path, storing the path data and final Op into ops.
//
// Caller must also call End to finish the drawing.
// Forgetting to call it will result in a "panic: cannot mix multi ops with single ones".
func (p *Path) Begin(o *op.Ops) { func (p *Path) Begin(o *op.Ops) {
*p = Path{ *p = Path{
ops: &o.Internal, ops: &o.Internal,
+3 -1
View File
@@ -4,6 +4,7 @@ package text
import ( import (
"bytes" "bytes"
"fmt"
"image" "image"
"io" "io"
"log" "log"
@@ -276,7 +277,8 @@ func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
// It returns whether the face is now available for use. FontFaces are prioritized // It returns whether the face is now available for use. FontFaces are prioritized
// in the order in which they are loaded, with the first face being the default. // in the order in which they are loaded, with the first face being the default.
func (s *shaperImpl) Load(f FontFace) { func (s *shaperImpl) Load(f FontFace) {
s.fontMap.AddFace(f.Face.Face(), opentype.FontToDescription(f.Font)) desc := opentype.FontToDescription(f.Font)
s.fontMap.AddFace(f.Face.Face(), fontscan.Location{File: fmt.Sprint(desc)}, desc)
s.addFace(f.Face.Face(), f.Font) s.addFace(f.Face.Face(), f.Font)
} }
+20 -13
View File
@@ -228,19 +228,19 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
axis = gesture.Vertical axis = gesture.Vertical
smin, smax = sbounds.Min.Y, sbounds.Max.Y smin, smax = sbounds.Min.Y, sbounds.Max.Y
} }
var scrollRange image.Rectangle var scrollX, scrollY pointer.ScrollRange
textDims := e.text.FullDimensions() textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions() visibleDims := e.text.Dimensions()
if e.SingleLine { if e.SingleLine {
scrollOffX := e.text.ScrollOff().X scrollOffX := e.text.ScrollOff().X
scrollRange.Min.X = min(-scrollOffX, 0) scrollX.Min = min(-scrollOffX, 0)
scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X)) scrollX.Max = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
} else { } else {
scrollOffY := e.text.ScrollOff().Y scrollOffY := e.text.ScrollOff().Y
scrollRange.Min.Y = -scrollOffY scrollY.Min = -scrollOffY
scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y)) scrollY.Max = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
} }
sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollRange) sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollX, scrollY)
var soff int var soff int
if e.SingleLine { if e.SingleLine {
e.text.ScrollRel(sdist, 0) e.text.ScrollRel(sdist, 0)
@@ -289,6 +289,9 @@ func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (Editor
Y: int(math.Round(float64(evt.Position.Y))), Y: int(math.Round(float64(evt.Position.Y))),
}) })
gtx.Execute(key.FocusCmd{Tag: e}) gtx.Execute(key.FocusCmd{Tag: e})
if !e.ReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
if e.scroller.State() != gesture.StateFlinging { if e.scroller.State() != gesture.StateFlinging {
e.scrollCaret = true e.scrollCaret = true
} }
@@ -312,8 +315,8 @@ func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (Editor
e.text.MoveWord(1, selectionExtend) e.text.MoveWord(1, selectionExtend)
e.dragging = false e.dragging = false
case evt.NumClicks >= 3: case evt.NumClicks >= 3:
e.text.MoveStart(selectionClear) e.text.MoveLineStart(selectionClear)
e.text.MoveEnd(selectionExtend) e.text.MoveLineEnd(selectionExtend)
e.dragging = false e.dragging = false
} }
} }
@@ -374,8 +377,8 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift}, key.Filter{Focus: e, Name: key.NameDeleteBackward, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift}, key.Filter{Focus: e, Name: key.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift}, key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShortcut | key.ModShift},
key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift}, key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShortcut | key.ModShift},
key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift}, key.Filter{Focus: e, Name: key.NamePageDown, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift}, key.Filter{Focus: e, Name: key.NamePageUp, Optional: key.ModShift},
condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}), condFilter(!atBeginning, key.Filter{Focus: e, Name: key.NameLeftArrow, Optional: key.ModShortcutAlt | key.ModShift}),
@@ -395,7 +398,7 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
case key.FocusEvent: case key.FocusEvent:
// Reset IME state. // Reset IME state.
e.ime.imeState = imeState{} e.ime.imeState = imeState{}
if ke.Focus { if ke.Focus && !e.ReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true}) gtx.Execute(key.SoftKeyboardCmd{Show: true})
} }
case key.Event: case key.Event:
@@ -521,6 +524,10 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
} }
} }
} }
case key.NameHome:
e.text.MoveTextStart(selAct)
case key.NameEnd:
e.text.MoveTextEnd(selAct)
} }
return nil, false return nil, false
} }
@@ -582,9 +589,9 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
case key.NamePageDown: case key.NamePageDown:
e.text.MovePages(+1, selAct) e.text.MovePages(+1, selAct)
case key.NameHome: case key.NameHome:
e.text.MoveStart(selAct) e.text.MoveLineStart(selAct)
case key.NameEnd: case key.NameEnd:
e.text.MoveEnd(selAct) e.text.MoveLineEnd(selAct)
} }
return nil, false return nil, false
} }
+77 -17
View File
@@ -256,7 +256,7 @@ func TestEditor(t *testing.T) {
// Regression test for bad in-cluster rune offset math. // Regression test for bad in-cluster rune offset math.
e.SetText("æbc") e.SetText("æbc")
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc")) assertCaret(t, e, 0, 3, len("æbc"))
textSample := "æbc\naøå••" textSample := "æbc\naøå••"
@@ -268,7 +268,7 @@ func TestEditor(t *testing.T) {
} }
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc")) assertCaret(t, e, 0, 3, len("æbc"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("æbc\n")) assertCaret(t, e, 1, 0, len("æbc\n"))
@@ -276,7 +276,7 @@ func TestEditor(t *testing.T) {
assertCaret(t, e, 0, 3, len("æbc")) assertCaret(t, e, 0, 3, len("æbc"))
e.text.MoveLines(+1, selectionClear) e.text.MoveLines(+1, selectionClear)
assertCaret(t, e, 1, 4, len("æbc\naøå•")) assertCaret(t, e, 1, 4, len("æbc\naøå•"))
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 1, 5, len("æbc\naøå••")) assertCaret(t, e, 1, 5, len("æbc\naøå••"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 5, len("æbc\naøå••")) assertCaret(t, e, 1, 5, len("æbc\naøå••"))
@@ -300,7 +300,7 @@ func TestEditor(t *testing.T) {
// Test that moveLine applies x offsets from previous moves. // Test that moveLine applies x offsets from previous moves.
e.SetText("long line\nshort") e.SetText("long line\nshort")
e.SetCaret(0, 0) e.SetCaret(0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
e.text.MoveLines(+1, selectionClear) e.text.MoveLines(+1, selectionClear)
e.text.MoveLines(-1, selectionClear) e.text.MoveLines(-1, selectionClear)
assertCaret(t, e, 0, utf8.RuneCountInString("long line"), len("long line")) assertCaret(t, e, 0, utf8.RuneCountInString("long line"), len("long line"))
@@ -342,14 +342,14 @@ func TestEditorRTL(t *testing.T) {
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 0, 3, len("الح")) assertCaret(t, e, 0, 3, len("الح"))
// Move to the "end" of the line. This moves to the left edge of the line. // Move to the "end" of the line. This moves to the left edge of the line.
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 4, len("الحب")) assertCaret(t, e, 0, 4, len("الحب"))
sentence := "الحب سماء لا\nتمط غير الأحلام" sentence := "الحب سماء لا\nتمط غير الأحلام"
e.SetText(sentence) e.SetText(sentence)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 12, len("الحب سماء لا")) assertCaret(t, e, 0, 12, len("الحب سماء لا"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("الحب سماء لا\n")) assertCaret(t, e, 1, 0, len("الحب سماء لا\n"))
@@ -361,7 +361,7 @@ func TestEditorRTL(t *testing.T) {
assertCaret(t, e, 0, 12, len("الحب سماء لا")) assertCaret(t, e, 0, 12, len("الحب سماء لا"))
e.text.MoveLines(+1, selectionClear) e.text.MoveLines(+1, selectionClear)
assertCaret(t, e, 1, 14, len("الحب سماء لا\nتمط غير الأحلا")) assertCaret(t, e, 1, 14, len("الحب سماء لا\nتمط غير الأحلا"))
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام")) assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام")) assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
@@ -417,7 +417,7 @@ func TestEditorLigature(t *testing.T) {
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.SetText("fl") // just a ligature e.SetText("fl") // just a ligature
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 2, len("fl")) assertCaret(t, e, 0, 2, len("fl"))
e.MoveCaret(-1, -1) e.MoveCaret(-1, -1)
assertCaret(t, e, 0, 1, len("f")) assertCaret(t, e, 0, 1, len("f"))
@@ -428,7 +428,7 @@ func TestEditorLigature(t *testing.T) {
e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1 e.SetText("flaffl•ffi\n•fflfi") // 3 ligatures on line 0, 2 on line 1
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0) assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 10, len("ffaffl•ffi")) assertCaret(t, e, 0, 10, len("ffaffl•ffi"))
e.MoveCaret(+1, +1) e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("ffaffl•ffi\n")) assertCaret(t, e, 1, 0, len("ffaffl•ffi\n"))
@@ -481,7 +481,7 @@ func TestEditorLigature(t *testing.T) {
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
// Ensure that all runes in the final cluster of a line are properly // Ensure that all runes in the final cluster of a line are properly
// decoded when moving to the end of the line. This is a regression test. // decoded when moving to the end of the line. This is a regression test.
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
// The first line was broken by line wrapping, not a newline character, and has a trailing // The first line was broken by line wrapping, not a newline character, and has a trailing
// whitespace. However, we should never be able to reach the "other side" of such a trailing // whitespace. However, we should never be able to reach the "other side" of such a trailing
// whitespace glyph. // whitespace glyph.
@@ -548,8 +548,10 @@ const (
moveRune moveRune
moveLine moveLine
movePage movePage
moveStart moveTextStart
moveEnd moveTextEnd
moveLineStart
moveLineEnd
moveCoord moveCoord
moveWord moveWord
deleteWord deleteWord
@@ -599,10 +601,14 @@ func TestEditorCaretConsistency(t *testing.T) {
e.text.MoveLines(int(distance), selectionClear) e.text.MoveLines(int(distance), selectionClear)
case movePage: case movePage:
e.text.MovePages(int(distance), selectionClear) e.text.MovePages(int(distance), selectionClear)
case moveStart: case moveLineStart:
e.text.MoveStart(selectionClear) e.text.MoveLineStart(selectionClear)
case moveEnd: case moveLineEnd:
e.text.MoveEnd(selectionClear) e.text.MoveLineEnd(selectionClear)
case moveTextStart:
e.text.MoveTextStart(selectionClear)
case moveTextEnd:
e.text.MoveTextEnd(selectionClear)
case moveCoord: case moveCoord:
e.text.MoveCoord(image.Pt(int(x), int(y))) e.text.MoveCoord(image.Pt(int(x), int(y)))
case moveWord: case moveWord:
@@ -879,7 +885,7 @@ func (editMutation) Generate(rand *rand.Rand, size int) reflect.Value {
// to make it much narrower (which makes the lines in the editor reflow), and // to make it much narrower (which makes the lines in the editor reflow), and
// then verifies that the updated (col, line) positions of the selected text // then verifies that the updated (col, line) positions of the selected text
// are where we expect. // are where we expect.
func TestEditorSelect(t *testing.T) { func TestEditorSelectReflow(t *testing.T) {
e := new(Editor) e := new(Editor)
e.SetText(`a 2 4 6 8 a e.SetText(`a 2 4 6 8 a
b 2 4 6 8 b b 2 4 6 8 b
@@ -982,6 +988,60 @@ g 2 4 6 8 g
} }
} }
func TestEditorSelectShortcuts(t *testing.T) {
tFont := font.Font{}
tFontSize := unit.Sp(10)
tShaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Collection()))
var tEditor = &Editor{
SingleLine: false,
ReadOnly: true,
}
lines := "abc abc abc\ndef def def\nghi ghi ghi"
tEditor.SetText(lines)
type testCase struct {
// Initial text selection.
startPos, endPos int
// Keyboard shortcut to execute.
keyEvent key.Event
// Expected text selection.
selection string
}
pos1, pos2 := 14, 21
for n, tst := range []testCase{
{pos1, pos2, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
{pos2, pos1, key.Event{Name: "A", Modifiers: key.ModShortcut}, lines},
{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "def def d"},
{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "ef"},
{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShift}, "de"},
{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShift}, "f def def"},
{pos1, pos2, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\ndef def d"},
{pos1, pos2, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "ef\nghi ghi ghi"},
{pos2, pos1, key.Event{Name: key.NameHome, Modifiers: key.ModShortcut | key.ModShift}, "abc abc abc\nde"},
{pos2, pos1, key.Event{Name: key.NameEnd, Modifiers: key.ModShortcut | key.ModShift}, "f def def\nghi ghi ghi"},
} {
tRouter := new(input.Router)
gtx := layout.Context{
Ops: new(op.Ops),
Locale: english,
Constraints: layout.Exact(image.Pt(100, 100)),
Source: tRouter.Source(),
}
gtx.Execute(key.FocusCmd{Tag: tEditor})
tEditor.Layout(gtx, tShaper, tFont, tFontSize, op.CallOp{}, op.CallOp{})
tEditor.SetCaret(tst.startPos, tst.endPos)
if cStart, cEnd := tEditor.Selection(); cStart != tst.startPos || cEnd != tst.endPos {
t.Errorf("TestEditorSelect %d: initial selection", n)
}
tRouter.Queue(tst.keyEvent)
tEditor.Update(gtx)
if got := tEditor.SelectedText(); got != tst.selection {
t.Errorf("TestEditorSelect %d: Expected %q, got %q", n, tst.selection, got)
}
}
}
// Verify that an existing selection is dismissed when you press arrow keys. // Verify that an existing selection is dismissed when you press arrow keys.
func TestSelectMove(t *testing.T) { func TestSelectMove(t *testing.T) {
e := new(Editor) e := new(Editor)
+8 -8
View File
@@ -284,9 +284,9 @@ func TestIndexPositionBidi(t *testing.T) {
name: "bidi rtl", name: "bidi rtl",
glyphs: bidiRTLText, glyphs: bidiRTLText,
expectedXs: []fixed.Int26_6{ expectedXs: []fixed.Int26_6{
2665, 3291, 3861, 4431, 4716, 5286, 5856, 6109, 6621, 7133, 2665, 2380, 1577, 985, 687, 266, // Positions on line 0. 2646, 3272, 3842, 4412, 4697, 5267, 5837, 6090, 6602, 7114, 2646, 2380, 1577, 985, 687, 266, // Positions on line 0.
7886, 7118, 6350, 5582, 4814, 4529, 4231, 3933, 3667, 2300, 2585, 3155, 3667, 2300, 2015, 1709, 1117, 266, // Positions on line 1. 7867, 7099, 6331, 5563, 4795, 4510, 4212, 3914, 3648, 2281, 2566, 3136, 3648, 2281, 2015, 1709, 1117, 266, // Positions on line 1.
8794, 8026, 7258, 6490, 5722, 5437, 4922, 4540, 4134, 3868, 0, 290, 860, 1430, 1715, 1989, 2559, 3071, 3583, // Positions on line 2. 8794, 8026, 7258, 6490, 5722, 5437, 4922, 4540, 4134, 3868, 0, 290, 860, 1430, 1715, 1989, 2559, 3071, 3583, // Positions on line 2.
@@ -402,7 +402,7 @@ func TestIndexPositionLines(t *testing.T) {
xOff: fixed.Int26_6(0), xOff: fixed.Int26_6(0),
yOff: 22, yOff: 22,
glyphs: 15, glyphs: 15,
width: fixed.Int26_6(7133), width: fixed.Int26_6(7114),
ascent: fixed.Int26_6(1407), ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756), descent: fixed.Int26_6(756),
}, },
@@ -410,7 +410,7 @@ func TestIndexPositionLines(t *testing.T) {
xOff: fixed.Int26_6(0), xOff: fixed.Int26_6(0),
yOff: 41, yOff: 41,
glyphs: 15, glyphs: 15,
width: fixed.Int26_6(7886), width: fixed.Int26_6(7867),
ascent: fixed.Int26_6(1407), ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756), descent: fixed.Int26_6(756),
}, },
@@ -477,18 +477,18 @@ func TestIndexPositionLines(t *testing.T) {
glyphs: bidiRTLTextOpp, glyphs: bidiRTLTextOpp,
expectedLines: []lineInfo{ expectedLines: []lineInfo{
{ {
xOff: fixed.Int26_6(3107), xOff: fixed.Int26_6(3126),
yOff: 22, yOff: 22,
glyphs: 15, glyphs: 15,
width: fixed.Int26_6(7133), width: fixed.Int26_6(7114),
ascent: fixed.Int26_6(1407), ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756), descent: fixed.Int26_6(756),
}, },
{ {
xOff: fixed.Int26_6(2354), xOff: fixed.Int26_6(2373),
yOff: 41, yOff: 41,
glyphs: 15, glyphs: 15,
width: fixed.Int26_6(7886), width: fixed.Int26_6(7867),
ascent: fixed.Int26_6(1407), ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756), descent: fixed.Int26_6(756),
}, },
+3
View File
@@ -54,6 +54,9 @@ func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions {
if !gtx.Enabled() { if !gtx.Enabled() {
fillColor = f32color.Disabled(fillColor) fillColor = f32color.Disabled(fillColor)
} }
if fillWidth < int(p.Radius*2) {
fillWidth = int(p.Radius * 2)
}
return shader(fillWidth, fillColor) return shader(fillWidth, fillColor)
}), }),
) )
+4 -4
View File
@@ -247,8 +247,8 @@ func (e *Selectable) processPointer(gtx layout.Context) {
e.text.MoveWord(1, selectionExtend) e.text.MoveWord(1, selectionExtend)
e.dragging = false e.dragging = false
case evt.NumClicks >= 3: case evt.NumClicks >= 3:
e.text.MoveStart(selectionClear) e.text.MoveLineStart(selectionClear)
e.text.MoveEnd(selectionExtend) e.text.MoveLineEnd(selectionExtend)
e.dragging = false e.dragging = false
} }
} }
@@ -378,9 +378,9 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) {
case key.NamePageDown: case key.NamePageDown:
e.text.MovePages(+1, selAct) e.text.MovePages(+1, selAct)
case key.NameHome: case key.NameHome:
e.text.MoveStart(selAct) e.text.MoveLineStart(selAct)
case key.NameEnd: case key.NameEnd:
e.text.MoveEnd(selAct) e.text.MoveLineEnd(selAct)
} }
} }
+23 -4
View File
@@ -639,9 +639,28 @@ func (e *textView) MoveCaret(startDelta, endDelta int) {
e.caret.end = e.moveByGraphemes(e.caret.end, endDelta) e.caret.end = e.moveByGraphemes(e.caret.end, endDelta)
} }
// MoveStart moves the caret to the start of the current line, ensuring that the resulting // MoveTextStart moves the caret to the start of the text.
func (e *textView) MoveTextStart(selAct selectionAction) {
caret := e.closestToRune(e.caret.end)
e.caret.start = 0
e.caret.end = caret.runes
e.caret.xoff = -caret.x
e.updateSelection(selAct)
e.clampCursorToGraphemes()
}
// MoveTextEnd moves the caret to the end of the text.
func (e *textView) MoveTextEnd(selAct selectionAction) {
caret := e.closestToRune(math.MaxInt)
e.caret.start = caret.runes
e.caret.xoff = fixed.I(e.params.MaxWidth) - caret.x
e.updateSelection(selAct)
e.clampCursorToGraphemes()
}
// MoveLineStart moves the caret to the start of the current line, ensuring that the resulting
// cursor position is on a grapheme cluster boundary. // cursor position is on a grapheme cluster boundary.
func (e *textView) MoveStart(selAct selectionAction) { func (e *textView) MoveLineStart(selAct selectionAction) {
caret := e.closestToRune(e.caret.start) caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, 0) caret = e.closestToLineCol(caret.lineCol.line, 0)
e.caret.start = caret.runes e.caret.start = caret.runes
@@ -650,9 +669,9 @@ func (e *textView) MoveStart(selAct selectionAction) {
e.clampCursorToGraphemes() e.clampCursorToGraphemes()
} }
// MoveEnd moves the caret to the end of the current line, ensuring that the resulting // MoveLineEnd moves the caret to the end of the current line, ensuring that the resulting
// cursor position is on a grapheme cluster boundary. // cursor position is on a grapheme cluster boundary.
func (e *textView) MoveEnd(selAct selectionAction) { func (e *textView) MoveLineEnd(selAct selectionAction) {
caret := e.closestToRune(e.caret.start) caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt) caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt)
e.caret.start = caret.runes e.caret.start = caret.runes