28 Commits

Author SHA1 Message Date
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
40 changed files with 510 additions and 291 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ environment:
tasks:
- install_go: |
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: |
mkdir -p $APPLE_TOOLCHAIN_ROOT
cd $APPLE_TOOLCHAIN_ROOT
+1 -1
View File
@@ -16,7 +16,7 @@ environment:
tasks:
- install_go: |
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: |
cd gio
go test ./...
+1 -1
View File
@@ -40,7 +40,7 @@ secrets:
tasks:
- install_go: |
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: |
cd gio
test -z "$(gofmt -s -l .)"
+1 -1
View File
@@ -10,7 +10,7 @@ environment:
tasks:
- install_go: |
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
./make.bash
- test_gio: |
+4 -6
View File
@@ -17,9 +17,8 @@ import (
)
type androidContext struct {
win *window
eglSurf egl.NativeWindowType
width, height int
win *window
eglSurf egl.NativeWindowType
*egl.Context
}
@@ -45,9 +44,8 @@ func (c *androidContext) Refresh() error {
if err := c.win.setVisual(c.Context.VisualID()); err != nil {
return err
}
win, width, height := c.win.nativeWindow()
win, _, _ := c.win.nativeWindow()
c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win))
c.width, c.height = width, height
return nil
}
@@ -55,7 +53,7 @@ func (c *androidContext) Lock() error {
// The Android emulator creates a broken surface if it is not
// created on the same thread as the context is made current.
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
}
c.eglSurf = nil
+1 -1
View File
@@ -69,7 +69,7 @@ func (c *wlContext) Refresh() error {
}
c.eglWin = 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
}
if err := c.Context.MakeCurrent(); err != nil {
+12 -17
View File
@@ -5,8 +5,6 @@
package app
import (
"golang.org/x/sys/windows"
"gioui.org/internal/egl"
)
@@ -24,6 +22,18 @@ func init() {
if err != nil {
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
},
})
@@ -37,21 +47,6 @@ func (c *glContext) Release() {
}
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
}
+12 -11
View File
@@ -25,6 +25,18 @@ func init() {
if err != nil {
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
}
}
@@ -37,17 +49,6 @@ func (c *x11Context) Release() {
}
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
}
+1 -1
View File
@@ -176,7 +176,7 @@ type context interface {
// 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 blocks until an event is available and returns it.
Event() event.Event
// Invalidate requests a FrameEvent.
Invalidate()
+5 -1
View File
@@ -75,9 +75,13 @@ var displayLinks sync.Map
var mainFuncs = make(chan func(), 1)
func isMainThread() bool {
return bool(C.isMainThread())
}
// runOnMain runs the function on the main thread.
func runOnMain(f func()) {
if C.isMainThread() {
if isMainThread() {
f()
return
}
+38 -1
View File
@@ -12,6 +12,7 @@ package app
#include <UIKit/UIKit.h>
#include <stdint.h>
__attribute__ ((visibility ("hidden"))) int gio_applicationMain(int argc, char *argv[]);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
struct drawParams {
@@ -81,6 +82,7 @@ import "C"
import (
"image"
"io"
"os"
"runtime"
"runtime/cgo"
"runtime/debug"
@@ -394,12 +396,47 @@ func newWindow(win *callbacks, options []Option) {
<-mainWindow.windows
}
var mainMode = mainModeUndefined
const (
mainModeUndefined = iota
mainModeExe
mainModeLibrary
)
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
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() {}
+20
View File
@@ -280,3 +280,23 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
GioView *v = (__bridge GioView *)viewRef;
v.handle = handle;
}
@interface _gioAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end
@implementation _gioAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
GioViewController *controller = [[GioViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = controller;
[self.window makeKeyAndVisible];
return YES;
}
@end
int gio_applicationMain(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([_gioAppDelegate class]));
}
}
+3
View File
@@ -976,6 +976,9 @@ func (w *window) init() error {
}
func osMain() {
if !isMainThread() {
panic("app.Main must run on the main goroutine")
}
C.gio_main()
}
+16 -4
View File
@@ -47,13 +47,17 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
}
- (void)windowDidBecomeKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 1);
GioView *view = (GioView *)window.contentView;
if ([window firstResponder] == view) {
gio_onFocus(view.handle, 1);
}
}
- (void)windowDidResignKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 0);
GioView *view = (GioView *)window.contentView;
if ([window firstResponder] == view) {
gio_onFocus(view.handle, 0);
}
}
@end
@@ -205,6 +209,14 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
- (void)dealloc {
gio_onDestroy(self.handle);
}
- (BOOL) becomeFirstResponder {
gio_onFocus(self.handle, 1);
return [super becomeFirstResponder];
}
- (BOOL) resignFirstResponder {
gio_onFocus(self.handle, 0);
return [super resignFirstResponder];
}
@end
// Delegates are weakly referenced from their peers. Nothing
+14 -10
View File
@@ -216,6 +216,8 @@ type window struct {
wakeups chan struct{}
closing bool
// invMu avoids the race between the destruction of disp and
// Invalidate waking it up.
invMu sync.Mutex
@@ -556,7 +558,7 @@ func gio_onXdgSurfaceConfigure(data unsafe.Pointer, wmSurf *C.struct_xdg_surface
//export gio_onToplevelClose
func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) {
w := callbackLoad(data).(*window)
w.close(nil)
w.closing = true
}
//export gio_onToplevelConfigure
@@ -1139,7 +1141,7 @@ func (w *window) Perform(actions system.Action) {
walkActions(actions, func(action system.Action) {
switch action {
case system.ActionClose:
w.close(nil)
w.closing = true
}
})
}
@@ -1366,6 +1368,11 @@ func gio_onFrameDone(data unsafe.Pointer, callback *C.struct_wl_callback, t C.ui
func (w *window) close(err error) {
w.ProcessEvent(WaylandViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
w.destroy()
w.invMu.Lock()
w.disp.destroy()
w.disp = nil
w.invMu.Unlock()
}
func (w *window) dispatch() {
@@ -1374,7 +1381,7 @@ func (w *window) dispatch() {
w.w.Invalidate()
return
}
if err := w.disp.dispatch(); err != nil {
if err := w.disp.dispatch(); err != nil || w.closing {
w.close(err)
return
}
@@ -1399,13 +1406,6 @@ func (w *window) Event() event.Event {
w.dispatch()
continue
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
w.invMu.Lock()
w.disp.destroy()
w.disp = nil
w.invMu.Unlock()
}
return evt
}
}
@@ -1515,6 +1515,10 @@ func (d *wlDisplay) wakeup() {
}
func (w *window) destroy() {
if w.lastFrameCallback != nil {
C.wl_callback_destroy(w.lastFrameCallback)
w.lastFrameCallback = nil
}
if w.cursor.surf != nil {
C.wl_surface_destroy(w.cursor.surf)
}
+10 -4
View File
@@ -389,6 +389,7 @@ func (w *x11Window) ProcessEvent(e event.Event) {
func (w *x11Window) shutdown(err error) {
w.ProcessEvent(X11ViewEvent{})
w.ProcessEvent(DestroyEvent{Err: err})
w.destroy()
}
func (w *x11Window) Event() event.Event {
@@ -398,9 +399,6 @@ func (w *x11Window) Event() event.Event {
w.dispatch()
continue
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
}
return evt
}
}
@@ -464,7 +462,12 @@ func (w *x11Window) dispatch() {
// Check for pending draw events before checking animation or blocking.
// This fixes an issue on Xephyr where on startup XPending() > 0 but
// poll will still block. This also prevents no-op calls to poll.
if syn = w.handler.handleEvents(); !syn {
syn = w.handler.handleEvents()
if w.x == nil {
// handleEvents received a close request and destroyed the window.
return
}
if !syn {
anim = w.animating
if !anim {
// Clear poll events.
@@ -476,6 +479,9 @@ func (w *x11Window) dispatch() {
switch {
case *xEvents&syscall.POLLIN != 0:
syn = w.handler.handleEvents()
if w.x == nil {
return
}
case *xEvents&(syscall.POLLERR|syscall.POLLHUP) != 0:
}
}
+62 -53
View File
@@ -9,7 +9,6 @@ import (
"image/color"
"reflect"
"runtime"
"sync"
"time"
"unicode/utf8"
@@ -36,14 +35,14 @@ type Option func(unit.Metric, *Config)
// Window represents an operating system window.
//
// The zero-value Window is useful, and calling any method on
// it creates and shows a new GUI window. On iOS or Android,
// the first Window represents the the window previously
// created by the platform.
// The zero-value Window is useful; the GUI window is created and shown the first
// time the [Event] method is called. On iOS or Android, the first Window represents
// the window previously created by the platform.
//
// More than one Window is not supported on iOS, Android,
// WebAssembly.
// More than one Window is not supported on iOS, Android, WebAssembly.
type Window struct {
initialOpts []Option
ctx context
gpu gpu.GPU
// timer tracks the delayed invalidate goroutine.
@@ -91,11 +90,10 @@ type Window struct {
driver driver
// basic is the driver interface that is needed even after the window is gone.
basic basicDriver
once sync.Once
// coalesced tracks the most recent events waiting to be delivered
// to the client.
coalesced eventSummary
// frame tracks the most recently frame event.
// frame tracks the most recent frame event.
lastFrame struct {
sync bool
size image.Point
@@ -273,8 +271,9 @@ func (w *Window) updateState() {
//
// Invalidate is safe for concurrent use.
func (w *Window) Invalidate() {
w.init()
w.basic.Invalidate()
if w.basic != nil {
w.basic.Invalidate()
}
}
// Option applies the options to the window. The options are hints; the platform is
@@ -283,7 +282,10 @@ func (w *Window) Option(opts ...Option) {
if len(opts) == 0 {
return
}
w.init(opts...)
if w.basic == nil {
w.initialOpts = append(w.initialOpts, opts...)
return
}
w.Run(func() {
cnf := Config{Decorated: w.decorations.enabled}
for _, opt := range opts {
@@ -302,13 +304,14 @@ func (w *Window) Option(opts ...Option) {
}
// Run f in the same thread as the native window event loop, and wait for f to
// return or the window to close.
// return or the window to close. If the window has not yet been created,
// Run calls f directly.
//
// Note that most programs should not call Run; configuring a Window with
// [CustomRenderer] is a notable exception.
func (w *Window) Run(f func()) {
w.init()
if w.driver == nil {
f()
return
}
done := make(chan struct{})
@@ -680,48 +683,50 @@ func (w *Window) processEvent(e event.Event) bool {
}
// 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 {
w.init()
if w.basic == nil {
w.init()
}
return w.basic.Event()
}
func (w *Window) init(initial ...Option) {
w.once.Do(func() {
debug.Parse()
// Measure decoration height.
deco := new(widget.Decorations)
theme := material.NewTheme()
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
decoStyle := material.Decorations(theme, deco, 0, "")
gtx := layout.Context{
Ops: new(op.Ops),
// Measure in Dp.
Metric: unit.Metric{},
}
// Allow plenty of space.
gtx.Constraints.Max.Y = 200
dims := decoStyle.Layout(gtx)
decoHeight := unit.Dp(dims.Size.Y)
defaultOptions := []Option{
Size(800, 600),
Title("Gio"),
Decorated(true),
decoHeightOpt(decoHeight),
}
options := append(defaultOptions, initial...)
var cnf Config
cnf.apply(unit.Metric{}, options)
func (w *Window) init() {
debug.Parse()
// Measure decoration height.
deco := new(widget.Decorations)
theme := material.NewTheme()
theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(gofont.Regular()))
decoStyle := material.Decorations(theme, deco, 0, "")
gtx := layout.Context{
Ops: new(op.Ops),
// Measure in Dp.
Metric: unit.Metric{},
}
// Allow plenty of space.
gtx.Constraints.Max.Y = 200
dims := decoStyle.Layout(gtx)
decoHeight := unit.Dp(dims.Size.Y)
defaultOptions := []Option{
Size(800, 600),
Title("Gio"),
Decorated(true),
decoHeightOpt(decoHeight),
}
options := append(defaultOptions, w.initialOpts...)
w.initialOpts = nil
var cnf Config
cnf.apply(unit.Metric{}, options)
w.nocontext = cnf.CustomRenderer
w.decorations.Theme = theme
w.decorations.Decorations = deco
w.decorations.enabled = cnf.Decorated
w.decorations.height = decoHeight
w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[input.SemanticID]input.SemanticNode)
newWindow(&callbacks{w}, options)
})
w.nocontext = cnf.CustomRenderer
w.decorations.Theme = theme
w.decorations.Decorations = deco
w.decorations.enabled = cnf.Decorated
w.decorations.height = decoHeight
w.imeState.compose = key.Range{Start: -1, End: -1}
w.semantic.ids = make(map[input.SemanticID]input.SemanticNode)
newWindow(&callbacks{w}, options)
}
func (w *Window) updateCursor() {
@@ -769,8 +774,12 @@ func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point {
}
// Update the window based on the actions on the decorations.
opts, acts := splitActions(deco.Update(gtx))
w.driver.Configure(opts)
w.driver.Perform(acts)
if len(opts) > 0 {
w.driver.Configure(opts)
}
if acts != 0 {
w.driver.Perform(acts)
}
style.Layout(gtx)
// Offset to place the frame content below the decorations.
decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
+5 -4
View File
@@ -271,12 +271,13 @@ func (s *Scroll) Stop() {
}
// 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
f := pointer.Filter{
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollBounds: bounds,
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollX: scrollx,
ScrollY: scrolly,
}
for {
evt, ok := q.Event(f)
+1 -1
View File
@@ -1,6 +1,6 @@
module gioui.org
go 1.19
go 1.21
require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
+1
View File
@@ -8,6 +8,7 @@ gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
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=
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=
+99 -89
View File
@@ -261,7 +261,7 @@ type texture struct {
type blitter struct {
ctx driver.Device
viewport image.Point
pipelines [3]*pipeline
pipelines [2][3]*pipeline
colUniforms *blitColUniforms
texUniforms *blitTexUniforms
linearGradientUniforms *blitLinearGradientUniforms
@@ -560,12 +560,24 @@ func newBlitter(ctx driver.Device) *blitter {
func (b *blitter) release() {
b.quadVerts.Release()
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) {
var pipelines [3]*pipeline
func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) (pipelines [2][3]*pipeline, err error) {
defer func() {
if err != nil {
for _, p := range pipelines {
for _, p := range p {
if p != nil {
p.Release()
}
}
}
}
}()
blend := driver.BlendDesc{
Enable: true,
SrcFactor: driver.BlendFactorOne,
@@ -583,86 +595,76 @@ func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.
return pipelines, err
}
defer vsh.Release()
{
fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
if err != nil {
return pipelines, err
for i, format := range []driver.TextureFormat{driver.TextureFormatOutput, driver.TextureFormatSRGBA} {
{
fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
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{
VertexShader: vsh,
FragmentShader: fsh,
BlendDesc: blend,
VertexLayout: layout,
PixelFormat: driver.TextureFormatOutput,
Topology: driver.TopologyTriangleStrip,
})
if err != nil {
return pipelines, err
{
var vertBuffer *uniformBuffer
fsh, err := b.NewFragmentShader(fsSrc[materialColor])
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[materialColor]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
pipelines[i][materialColor] = &pipeline{pipe, vertBuffer}
}
var vertBuffer *uniformBuffer
if u := uniforms[materialTexture]; u != nil {
vertBuffer = newUniformBuffer(b, u)
{
var vertBuffer *uniformBuffer
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
}
@@ -865,7 +867,7 @@ func (r *renderer) drawLayers(layers []opacityLayer, ops []imageOp) {
Min: l.place.Pos,
Max: l.place.Pos.Add(l.clip.Size()),
}
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y)
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Dx(), v.Dy())
f := r.layerFBOs.fbos[fbo]
r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
sr := f32.FRect(v)
@@ -930,7 +932,7 @@ func (d *drawOps) newPathOp() *pathOp {
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 = pathOp{
parent: state.cpath,
@@ -1055,7 +1057,7 @@ loop:
quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans)
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{}
case ops.TypePopClip:
state.cpath = state.cpath.parent
@@ -1100,7 +1102,7 @@ loop:
// this transformed rectangle.
k := opKey{Key: encOp.Key}
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()
@@ -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
for i := 0; i < len(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)
var fbo FBO
fboIdx := 0
if isFBO {
fboIdx = 1
}
switch img.clipType {
case clipTypeNone:
p := r.blitter.pipelines[m.material]
p := r.blitter.pipelines[fboIdx][m.material]
r.ctx.BindPipeline(p.pipeline)
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)
@@ -1265,7 +1271,7 @@ func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point,
Max: img.place.Pos.Add(drc.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.BindVertexBuffer(r.blitter.quadVerts, 0)
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) {
p := b.pipelines[mat]
fboIdx := 0
if fbo {
fboIdx = 1
}
p := b.pipelines[fboIdx][mat]
b.ctx.BindPipeline(p.pipeline)
var uniforms *blitUniforms
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) {
run(t, func(ops *op.Ops) {
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())
opc2 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op())
opc2.Pop()
opc1.Pop()
opc3 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(50+20, 10), Max: image.Pt(50+64, 128)}.Op())
paint.FillShape(ops, color.NRGBA{B: 255, A: 255}, clip.Ellipse(image.Rectangle{Min: image.Pt(20+20, 10), Max: image.Pt(50+64, 128)}).Op(ops))
opc3.Pop()
}, func(r result) {
})
+9 -3
View File
@@ -30,7 +30,7 @@ type pather struct {
type coverer struct {
ctx driver.Device
pipelines [3]*pipeline
pipelines [2][3]*pipeline
texUniforms *coverTexUniforms
colUniforms *coverColUniforms
linearGradientUniforms *coverLinearGradientUniforms
@@ -309,7 +309,9 @@ func (p *pather) release() {
func (c *coverer) release() {
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.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)
}
+4 -7
View File
@@ -15,10 +15,9 @@ import (
)
type Context struct {
disp _EGLDisplay
eglCtx *eglContext
eglSurf _EGLSurface
width, height int
disp _EGLDisplay
eglCtx *eglContext
eglSurf _EGLSurface
}
type eglContext struct {
@@ -121,11 +120,9 @@ func (c *Context) VisualID() int {
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)
c.eglSurf = eglSurf
c.width = width
c.height = height
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 {
n := c.GetProgrami(p, INFO_LOG_LENGTH)
if n == 0 {
return ""
}
buf := make([]byte, n)
syscall.Syscall6(_glGetProgramInfoLog.Addr(), 4, uintptr(p.V), uintptr(len(buf)), 0, uintptr(unsafe.Pointer(&buf[0])), 0, 0)
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 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))
l64 := float64(l)
if math.Abs(d-l64) < 1e-10 {
+4 -3
View File
@@ -251,9 +251,10 @@ func TestFocusScroll(t *testing.T) {
filters := []event.Filter{
key.FocusFilter{Target: h},
pointer.Filter{
Target: h,
Kinds: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100),
Target: h,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Min: -100, Max: +100},
ScrollY: pointer.ScrollRange{Min: -100, Max: +100},
},
}
events(r, -1, filters...)
+7 -5
View File
@@ -72,7 +72,7 @@ type pointerHandler struct {
type pointerFilter struct {
kinds pointer.Kind
// min and max horizontal/vertical scroll
scrollRange image.Rectangle
scrollX, scrollY pointer.ScrollRange
sourceMimes []string
targetMimes []string
@@ -297,7 +297,8 @@ func (p *pointerFilter) Add(f event.Filter) {
p.targetMimes = append(p.targetMimes, f.Type)
case pointer.Filter:
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) {
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.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
// scroll accepted by the filter.
func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) {
left.X, scrolled.X = clampSplit(scroll.X, p.scrollRange.Min.X, p.scrollRange.Max.X)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollRange.Min.Y, p.scrollRange.Max.Y)
left.X, scrolled.X = clampSplit(scroll.X, p.scrollX.Min, p.scrollX.Max)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max)
return
}
+10 -9
View File
@@ -300,9 +300,9 @@ func TestPointerPriority(t *testing.T) {
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
f1 := func(t event.Tag) event.Filter {
return pointer.Filter{
Target: t,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 100}},
Target: t,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Max: 100},
}
}
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)
f2 := func(t event.Tag) event.Filter {
return pointer.Filter{
Target: t,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
Target: t,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Max: 20},
}
}
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)
f3 := func(t event.Tag) event.Filter {
return pointer.Filter{
Target: t,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
Target: t,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Min: -20},
ScrollY: pointer.ScrollRange{Min: -40},
}
}
events(&r, -1, f3(handler3))
+3 -5
View File
@@ -35,10 +35,9 @@ type Router struct {
queue keyQueue
// The following fields have the same purpose as the fields in
// type handler, but for key.Events.
filter keyFilter
nextFilter keyFilter
processedFilter keyFilter
scratchFilter keyFilter
filter keyFilter
nextFilter keyFilter
scratchFilter keyFilter
}
cqueue clipboardQueue
// states is the list of pending state changes resulting from
@@ -304,7 +303,6 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
h := q.stateFor(tf.tag)
h.processedFilter.Merge(tf.filter)
}
q.key.processedFilter = append(q.key.processedFilter, q.key.scratchFilter...)
return nil, false
}
+2 -2
View File
@@ -37,11 +37,11 @@ For example:
var h1, h2 *Handler
area := clip.Rect(...).Push(ops)
event.Op{Tag: h1}.Add(Ops)
event.Op(Ops, h1)
area.Pop()
area := clip.Rect(...).Push(ops)
event.Op{Tag: h2}.Add(ops)
event.Op(Ops, h2)
area.Pop()
implies a tree of two inner nodes, each with one pointer handler attached.
+19 -6
View File
@@ -3,7 +3,6 @@
package pointer
import (
"image"
"strings"
"time"
@@ -61,12 +60,19 @@ type Filter struct {
Target event.Tag
// Kinds is a bitwise-or of event types to match.
Kinds Kind
// ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy
// ScrollX and ScrollY constrain the range of scrolling events delivered
// to Target. Specifically, any Event e delivered to Tag will satisfy
//
// ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis)
// ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis)
ScrollBounds image.Rectangle
// ScrollX.Min <= e.Scroll.X <= ScrollX.Max (horizontal axis)
// ScrollY.Min <= e.Scroll.Y <= ScrollY.Max (vertical axis)
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.
@@ -219,6 +225,13 @@ const (
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.
func (p PassOp) Push(o *op.Ops) PassStack {
id, mid := ops.PushOp(&o.Internal, ops.PassStack)
+6 -4
View File
@@ -7,6 +7,7 @@ import (
"math"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/op"
"gioui.org/op/clip"
)
@@ -158,11 +159,12 @@ func (l *List) update(gtx Context) {
max = 0
}
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
xrange := pointer.ScrollRange{Min: min, Max: max}
yrange := pointer.ScrollRange{}
if l.Axis == Vertical {
xrange, yrange = yrange, xrange
}
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange)
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), xrange, yrange)
l.scrollDelta = d
l.Position.Offset += d
}
+3
View File
@@ -138,6 +138,9 @@ type Path struct {
func (p *Path) Pos() f32.Point { return p.pen }
// 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) {
*p = Path{
ops: &o.Internal,
+20 -13
View File
@@ -228,19 +228,19 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
axis = gesture.Vertical
smin, smax = sbounds.Min.Y, sbounds.Max.Y
}
var scrollRange image.Rectangle
var scrollX, scrollY pointer.ScrollRange
textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions()
if e.SingleLine {
scrollOffX := e.text.ScrollOff().X
scrollRange.Min.X = min(-scrollOffX, 0)
scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
scrollX.Min = min(-scrollOffX, 0)
scrollX.Max = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
} else {
scrollOffY := e.text.ScrollOff().Y
scrollRange.Min.Y = -scrollOffY
scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
scrollY.Min = -scrollOffY
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
if e.SingleLine {
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))),
})
gtx.Execute(key.FocusCmd{Tag: e})
if !e.ReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
if e.scroller.State() != gesture.StateFlinging {
e.scrollCaret = true
}
@@ -312,8 +315,8 @@ func (e *Editor) processPointerEvent(gtx layout.Context, ev event.Event) (Editor
e.text.MoveWord(1, selectionExtend)
e.dragging = false
case evt.NumClicks >= 3:
e.text.MoveStart(selectionClear)
e.text.MoveEnd(selectionExtend)
e.text.MoveLineStart(selectionClear)
e.text.MoveLineEnd(selectionExtend)
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.NameDeleteForward, Optional: key.ModShortcutAlt | key.ModShift},
key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShift},
key.Filter{Focus: e, Name: key.NameHome, Optional: key.ModShortcut | 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.NamePageUp, Optional: 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:
// Reset IME state.
e.ime.imeState = imeState{}
if ke.Focus {
if ke.Focus && !e.ReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
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
}
@@ -582,9 +589,9 @@ func (e *Editor) command(gtx layout.Context, k key.Event) (EditorEvent, bool) {
case key.NamePageDown:
e.text.MovePages(+1, selAct)
case key.NameHome:
e.text.MoveStart(selAct)
e.text.MoveLineStart(selAct)
case key.NameEnd:
e.text.MoveEnd(selAct)
e.text.MoveLineEnd(selAct)
}
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.
e.SetText("æbc")
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"))
textSample := "æbc\naøå••"
@@ -268,7 +268,7 @@ func TestEditor(t *testing.T) {
}
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 3, len("æbc"))
e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("æbc\n"))
@@ -276,7 +276,7 @@ func TestEditor(t *testing.T) {
assertCaret(t, e, 0, 3, len("æbc"))
e.text.MoveLines(+1, selectionClear)
assertCaret(t, e, 1, 4, len("æbc\naøå•"))
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 1, 5, len("æbc\naøå••"))
e.MoveCaret(+1, +1)
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.
e.SetText("long line\nshort")
e.SetCaret(0, 0)
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
e.text.MoveLines(+1, selectionClear)
e.text.MoveLines(-1, selectionClear)
assertCaret(t, e, 0, utf8.RuneCountInString("long line"), len("long line"))
@@ -342,14 +342,14 @@ func TestEditorRTL(t *testing.T) {
e.MoveCaret(+1, +1)
assertCaret(t, e, 0, 3, len("الح"))
// 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("الحب"))
sentence := "الحب سماء لا\nتمط غير الأحلام"
e.SetText(sentence)
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 12, len("الحب سماء لا"))
e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 0, len("الحب سماء لا\n"))
@@ -361,7 +361,7 @@ func TestEditorRTL(t *testing.T) {
assertCaret(t, e, 0, 12, len("الحب سماء لا"))
e.text.MoveLines(+1, selectionClear)
assertCaret(t, e, 1, 14, len("الحب سماء لا\nتمط غير الأحلا"))
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
e.MoveCaret(+1, +1)
assertCaret(t, e, 1, 15, len("الحب سماء لا\nتمط غير الأحلام"))
@@ -417,7 +417,7 @@ func TestEditorLigature(t *testing.T) {
assertCaret(t, e, 0, 0, 0)
e.SetText("fl") // just a ligature
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"))
e.MoveCaret(-1, -1)
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.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
assertCaret(t, e, 0, 0, 0)
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
assertCaret(t, e, 0, 10, len("ffaffl•ffi"))
e.MoveCaret(+1, +1)
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{})
// 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.
e.text.MoveEnd(selectionClear)
e.text.MoveLineEnd(selectionClear)
// 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 glyph.
@@ -548,8 +548,10 @@ const (
moveRune
moveLine
movePage
moveStart
moveEnd
moveTextStart
moveTextEnd
moveLineStart
moveLineEnd
moveCoord
moveWord
deleteWord
@@ -599,10 +601,14 @@ func TestEditorCaretConsistency(t *testing.T) {
e.text.MoveLines(int(distance), selectionClear)
case movePage:
e.text.MovePages(int(distance), selectionClear)
case moveStart:
e.text.MoveStart(selectionClear)
case moveEnd:
e.text.MoveEnd(selectionClear)
case moveLineStart:
e.text.MoveLineStart(selectionClear)
case moveLineEnd:
e.text.MoveLineEnd(selectionClear)
case moveTextStart:
e.text.MoveTextStart(selectionClear)
case moveTextEnd:
e.text.MoveTextEnd(selectionClear)
case moveCoord:
e.text.MoveCoord(image.Pt(int(x), int(y)))
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
// then verifies that the updated (col, line) positions of the selected text
// are where we expect.
func TestEditorSelect(t *testing.T) {
func TestEditorSelectReflow(t *testing.T) {
e := new(Editor)
e.SetText(`a 2 4 6 8 a
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.
func TestSelectMove(t *testing.T) {
e := new(Editor)
+3
View File
@@ -54,6 +54,9 @@ func (p ProgressBarStyle) Layout(gtx layout.Context) layout.Dimensions {
if !gtx.Enabled() {
fillColor = f32color.Disabled(fillColor)
}
if fillWidth < int(p.Radius*2) {
fillWidth = int(p.Radius * 2)
}
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.dragging = false
case evt.NumClicks >= 3:
e.text.MoveStart(selectionClear)
e.text.MoveEnd(selectionExtend)
e.text.MoveLineStart(selectionClear)
e.text.MoveLineEnd(selectionExtend)
e.dragging = false
}
}
@@ -378,9 +378,9 @@ func (e *Selectable) command(gtx layout.Context, k key.Event) {
case key.NamePageDown:
e.text.MovePages(+1, selAct)
case key.NameHome:
e.text.MoveStart(selAct)
e.text.MoveLineStart(selAct)
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)
}
// 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.
func (e *textView) MoveStart(selAct selectionAction) {
func (e *textView) MoveLineStart(selAct selectionAction) {
caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, 0)
e.caret.start = caret.runes
@@ -650,9 +669,9 @@ func (e *textView) MoveStart(selAct selectionAction) {
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.
func (e *textView) MoveEnd(selAct selectionAction) {
func (e *textView) MoveLineEnd(selAct selectionAction) {
caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt)
e.caret.start = caret.runes