Compare commits

..

5 Commits

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

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

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

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

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

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

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:52:04 +00:00
Elias Naur 9fe8b684e2 app: introduce Config.Focused that tracks the window focus state
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:52:04 +00:00
Elias Naur 2a18a0c135 app: [macOS] synchronize rendering with Core Animation for smooth resizes
Magic incantations lifted from

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

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2024-02-08 18:52:04 +00:00
71 changed files with 3827 additions and 1634 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ environment:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl -s https://dl.google.com/go/go1.22.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
curl -s https://dl.google.com/go/go1.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- prepare_toolchain: |
mkdir -p $APPLE_TOOLCHAIN_ROOT
cd $APPLE_TOOLCHAIN_ROOT
@@ -71,4 +71,4 @@ tasks:
CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-macos GOOS=darwin CGO_ENABLED=1 go build ./...
- test_ios: |
cd gio
CGO_CFLAGS=-Wno-deprecated-module-dot-map CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-ios GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags ios ./...
CC=$APPLE_TOOLCHAIN_ROOT/tools/clang-ios GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 go build -tags ios ./...
+2 -2
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Unlicense OR MIT
image: freebsd/latest
image: freebsd/13.x
packages:
- libX11
- libxkbcommon
@@ -16,7 +16,7 @@ environment:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.22.2.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
curl https://dl.google.com/go/go1.19.11.freebsd-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- test_gio: |
cd gio
go test ./...
+1 -3
View File
@@ -40,7 +40,7 @@ secrets:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl -s https://dl.google.com/go/go1.22.2.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
curl -s https://dl.google.com/go/go1.19.11.linux-amd64.tar.gz | tar -C /home/build/sdk -xzf -
- check_gofmt: |
cd gio
test -z "$(gofmt -s -l .)"
@@ -80,8 +80,6 @@ tasks:
unzip -q ndk.zip
rm ndk.zip
mv android-ndk-* ndk-bundle
# sdkmanager needs lots of file descriptors
ulimit -n 10000
yes|sdkmanager --licenses
sdkmanager "platforms;android-31" "build-tools;32.0.0"
- test_android: |
+1 -1
View File
@@ -10,7 +10,7 @@ environment:
tasks:
- install_go: |
mkdir -p /home/build/sdk
curl https://dl.google.com/go/go1.22.2.src.tar.gz | tar -C /home/build/sdk -xzf -
curl https://dl.google.com/go/go1.19.11.src.tar.gz | tar -C /home/build/sdk -xzf -
cd /home/build/sdk/go/src
./make.bash
- test_gio: |
-4
View File
@@ -259,10 +259,6 @@ public final class GioView extends SurfaceView implements Choreographer.FrameCal
}
private void setHighRefreshRate() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return;
}
Context context = getContext();
Display display = context.getDisplay();
Display.Mode[] supportedModes = display.getSupportedModes();
-16
View File
@@ -61,10 +61,6 @@ type FrameEvent struct {
type ViewEvent interface {
implementsViewEvent()
ImplementsEvent()
// Valid will return true when the ViewEvent does contains valid handles.
// If a window receives an invalid ViewEvent, it should deinitialize any
// state referring to handles from a previous ViewEvent.
Valid() bool
}
// Insets is the space taken up by
@@ -124,18 +120,6 @@ func DataDir() (string, error) {
return dataDir()
}
// Main must be called last from the program main function.
// On most platforms Main blocks forever, for Android and
// iOS it returns immediately to give control of the main
// thread back to the system.
//
// Calling Main is necessary because some operating systems
// require control of the main thread of the program for
// running windows.
func Main() {
osMain()
}
func (FrameEvent) ImplementsEvent() {}
func init() {
+5 -21
View File
@@ -33,28 +33,12 @@ For example:
A program must keep receiving events from the event channel until
[DestroyEvent] is received.
# Main
# Main Thread
The Main function must be called from a program's main function, to hand over
control of the main thread to operating systems that need it.
Because Main is also blocking on some platforms, the event loop of a Window must run in a goroutine.
For example, to display a blank but otherwise functional window:
package main
import "gioui.org/app"
func main() {
go func() {
w := app.NewWindow()
for {
w.Event()
}
}()
app.Main()
}
Some GUI platform need access to the main thread of the program. To avoid a
deadlock on such platforms, at least one Window must have its Event method
called by the main goroutine. It doesn't have to be any particular Window;
even a destroyed Window suffices.
# Permissions
+6 -4
View File
@@ -17,8 +17,9 @@ import (
)
type androidContext struct {
win *window
eglSurf egl.NativeWindowType
win *window
eglSurf egl.NativeWindowType
width, height int
*egl.Context
}
@@ -44,8 +45,9 @@ func (c *androidContext) Refresh() error {
if err := c.win.setVisual(c.Context.VisualID()); err != nil {
return err
}
win, _, _ := c.win.nativeWindow()
win, width, height := c.win.nativeWindow()
c.eglSurf = egl.NativeWindowType(unsafe.Pointer(win))
c.width, c.height = width, height
return nil
}
@@ -53,7 +55,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); err != nil {
if err := c.Context.CreateSurface(c.eglSurf, c.width, c.height); 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); err != nil {
if err := c.Context.CreateSurface(eglSurf, width, height); err != nil {
return err
}
if err := c.Context.MakeCurrent(); err != nil {
+17 -12
View File
@@ -5,6 +5,8 @@
package app
import (
"golang.org/x/sys/windows"
"gioui.org/internal/egl"
)
@@ -22,18 +24,6 @@ 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
},
})
@@ -47,6 +37,21 @@ 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
}
+11 -12
View File
@@ -25,18 +25,6 @@ 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
}
}
@@ -49,6 +37,17 @@ 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
@@ -7,7 +7,7 @@
#include <OpenGL/OpenGL.h>
#include "_cgo_export.h"
CALayer *gio_layerFactory(BOOL presentWithTrans) {
CALayer *gio_layerFactory(void) {
@autoreleasepool {
return [CALayer layer];
}
-1
View File
@@ -274,7 +274,6 @@ const (
WM_SETFOCUS = 0x0007
WM_SHOWWINDOW = 0x0018
WM_SIZE = 0x0005
WM_STYLECHANGED = 0x007D
WM_SYSKEYDOWN = 0x0104
WM_SYSKEYUP = 0x0105
WM_RBUTTONDOWN = 0x0204
+2 -3
View File
@@ -4,15 +4,14 @@ package app
import (
"log"
"syscall"
"unsafe"
syscall "golang.org/x/sys/windows"
)
type logger struct{}
var (
kernel32 = syscall.NewLazySystemDLL("kernel32")
kernel32 = syscall.NewLazyDLL("kernel32")
outputDebugStringW = kernel32.NewProc("OutputDebugStringW")
debugView *logger
)
+2 -2
View File
@@ -12,12 +12,12 @@ package app
#import <QuartzCore/CAMetalLayer.h>
#include <CoreFoundation/CoreFoundation.h>
CALayer *gio_layerFactory(BOOL presentWithTrans) {
CALayer *gio_layerFactory(void) {
@autoreleasepool {
CAMetalLayer *l = [CAMetalLayer layer];
l.autoresizingMask = kCALayerHeightSizable|kCALayerWidthSizable;
l.needsDisplayOnBoundsChange = YES;
l.presentsWithTransaction = presentWithTrans;
l.presentsWithTransaction = YES;
return l;
}
}
+12 -4
View File
@@ -173,13 +173,19 @@ type context interface {
Unlock()
}
// driver is the interface for the platform implementation
// of a window.
type driver interface {
// Event blocks until an event is available and returns it.
// basicDriver is the subset of [driver] that may be called even after
// a window is destroyed.
type basicDriver interface {
// Event blocks until an even is available and returns it.
Event() event.Event
// Invalidate requests a FrameEvent.
Invalidate()
}
// driver is the interface for the platform implementation
// of a window.
type driver interface {
basicDriver
// SetAnimating sets the animation flag. When the window is animating,
// FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool)
@@ -195,6 +201,8 @@ type driver interface {
Configure([]Option)
// SetCursor updates the current cursor to name.
SetCursor(cursor pointer.Cursor)
// Wakeup wakes up the event loop and sends a WakeupEvent.
// Wakeup()
// Perform actions on the window.
Perform(system.Action)
// EditorStateChanged notifies the driver that the editor state changed.
+4 -10
View File
@@ -575,7 +575,10 @@ func Java_org_gioui_GioView_onFrameCallback(env *C.JNIEnv, class C.jclass, view
if !exist {
return
}
w.draw(env, false)
if w.visible && w.animating {
w.draw(env, false)
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
}
//export Java_org_gioui_GioView_onBack
@@ -879,9 +882,6 @@ func (w *window) draw(env *C.JNIEnv, sync bool) {
},
Sync: sync,
})
if w.animating {
callVoidMethod(env, w.view, gioView.postFrameCallback)
}
a11yActive, err := callBooleanMethod(env, w.view, gioView.isA11yActive)
if err != nil {
panic(err)
@@ -1317,9 +1317,6 @@ func findClass(env *C.JNIEnv, name string) C.jclass {
return C.jni_FindClass(env, cn)
}
func osMain() {
}
func newWindow(window *callbacks, options []Option) {
mainWindow.in <- windowAndConfig{window, options}
<-mainWindow.windows
@@ -1495,6 +1492,3 @@ func Java_org_gioui_Gio_scheduleMainFuncs(env *C.JNIEnv, cls C.jclass) {
func (AndroidViewEvent) implementsViewEvent() {}
func (AndroidViewEvent) ImplementsEvent() {}
func (a AndroidViewEvent) Valid() bool {
return a != (AndroidViewEvent{})
}
+6 -13
View File
@@ -15,8 +15,8 @@ __attribute__ ((visibility ("hidden"))) void gio_hideCursor();
__attribute__ ((visibility ("hidden"))) void gio_showCursor();
__attribute__ ((visibility ("hidden"))) void gio_setCursor(NSUInteger curID);
static bool isMainThread() {
return [NSThread isMainThread];
static int isMainThread() {
return [NSThread isMainThread] ? 1 : 0;
}
static NSUInteger nsstringLength(CFTypeRef cstr) {
@@ -75,10 +75,6 @@ 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 isMainThread() {
@@ -91,6 +87,10 @@ func runOnMain(f func()) {
}()
}
func isMainThread() bool {
return C.isMainThread() != 0
}
//export gio_dispatchMainFuncs
func gio_dispatchMainFuncs() {
for {
@@ -263,10 +263,3 @@ func windowSetCursor(from, to pointer.Cursor) pointer.Cursor {
C.gio_setCursor(C.NSUInteger(macosCursorID[to]))
return to
}
func (w *window) wakeup() {
runOnMain(func() {
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
-11
View File
@@ -1,11 +0,0 @@
// SPDX-License-Identifier: Unlicense OR MIT
#import <Foundation/Foundation.h>
#include "_cgo_export.h"
void gio_wakeupMainThread(void) {
dispatch_async(dispatch_get_main_queue(), ^{
gio_dispatchMainFuncs();
});
}
+8 -50
View File
@@ -12,7 +12,6 @@ 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 {
@@ -22,7 +21,6 @@ struct drawParams {
};
static void writeClipboard(unichar *chars, NSUInteger length) {
#if !TARGET_OS_TV
@autoreleasepool {
NSString *s = [NSString string];
if (length > 0) {
@@ -31,18 +29,13 @@ static void writeClipboard(unichar *chars, NSUInteger length) {
UIPasteboard *p = UIPasteboard.generalPasteboard;
p.string = s;
}
#endif
}
static CFTypeRef readClipboard(void) {
#if !TARGET_OS_TV
@autoreleasepool {
UIPasteboard *p = UIPasteboard.generalPasteboard;
return (__bridge_retained CFTypeRef)p.string;
}
#else
return nil;
#endif
}
static void showTextInput(CFTypeRef viewRef) {
@@ -82,7 +75,6 @@ import "C"
import (
"image"
"io"
"os"
"runtime"
"runtime/cgo"
"runtime/debug"
@@ -396,51 +388,17 @@ 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() {
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.
}
runMain()
}
func (w *window) wakeup() {
runOnMain(func() {
w.loop.Wakeup()
w.loop.FlushEvents()
})
}
func (UIKitViewEvent) implementsViewEvent() {}
func (UIKitViewEvent) ImplementsEvent() {}
func (u UIKitViewEvent) Valid() bool {
return u != (UIKitViewEvent{})
}
+5 -23
View File
@@ -26,13 +26,12 @@ CGFloat _keyboardHeight;
self.view.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
UIView *drawView = [[GioView alloc] initWithFrame:zeroFrame];
[self.view addSubview: drawView];
#if !TARGET_OS_TV
#ifndef TARGET_OS_TV
drawView.multipleTouchEnabled = YES;
#endif
drawView.preservesSuperviewLayoutMargins = YES;
drawView.layoutMargins = UIEdgeInsetsMake(0, 0, 0, 0);
onCreate((__bridge CFTypeRef)drawView, (__bridge CFTypeRef)self);
#if !TARGET_OS_TV
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillChange:)
name:UIKeyboardWillShowNotification
@@ -45,7 +44,6 @@ CGFloat _keyboardHeight;
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
#endif
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(applicationDidEnterBackground:)
name: UIApplicationDidEnterBackgroundNotification
@@ -91,7 +89,6 @@ CGFloat _keyboardHeight;
[super didReceiveMemoryWarning];
}
#if !TARGET_OS_TV
- (void)keyboardWillChange:(NSNotification *)note {
NSDictionary *userInfo = note.userInfo;
CGRect f = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
@@ -103,7 +100,6 @@ CGFloat _keyboardHeight;
_keyboardHeight = 0.0;
[self.view setNeedsLayout];
}
#endif
@end
static void handleTouches(int last, GioView *view, NSSet<UITouch *> *touches, UIEvent *event) {
@@ -281,22 +277,8 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t 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]));
}
void gio_wakeupMainThread(void) {
dispatch_async(dispatch_get_main_queue(), ^{
gio_dispatchMainFuncs();
});
}
-7
View File
@@ -741,10 +741,6 @@ func (w *window) navigationColor(c color.NRGBA) {
theme.Set("content", fmt.Sprintf("#%06X", []uint8{rgba.R, rgba.G, rgba.B}))
}
func osMain() {
select {}
}
func translateKey(k string) (key.Name, bool) {
var n key.Name
@@ -822,6 +818,3 @@ func translateKey(k string) (key.Name, bool) {
func (JSViewEvent) implementsViewEvent() {}
func (JSViewEvent) ImplementsEvent() {}
func (j JSViewEvent) Valid() bool {
return !(j.Element.IsNull() || j.Element.IsUndefined())
}
+208 -267
View File
@@ -39,8 +39,8 @@ import (
#define MOUSE_DOWN 3
#define MOUSE_SCROLL 4
__attribute__ ((visibility ("hidden"))) void gio_main(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(int presentWithTrans);
__attribute__ ((visibility ("hidden"))) void gio_initApp(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createView(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGFloat minWidth, CGFloat minHeight, CGFloat maxWidth, CGFloat maxHeight);
__attribute__ ((visibility ("hidden"))) void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle);
@@ -109,14 +109,6 @@ static void makeKeyAndOrderFront(CFTypeRef windowRef) {
}
}
static void makeFirstResponder(CFTypeRef windowRef, CFTypeRef viewRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
NSView *view = (__bridge NSView *)viewRef;
[window makeFirstResponder:view];
}
}
static void toggleFullScreen(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
@@ -203,14 +195,6 @@ 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) {
@autoreleasepool {
NSWindow* window = (__bridge NSWindow *)windowRef;
@@ -246,13 +230,6 @@ static int isWindowZoomed(CFTypeRef windowRef) {
}
}
static int isWindowMiniaturized(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return window.miniaturized ? 1 : 0;
}
}
static void zoomWindow(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
@@ -313,18 +290,13 @@ static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
}
}
static void interpretKeyEvents(CFTypeRef viewRef, CFTypeRef eventRef) {
static void dispatchEvent(void) {
@autoreleasepool {
NSView *view = (__bridge NSView *)viewRef;
NSEvent *event = (__bridge NSEvent *)eventRef;
[view interpretKeyEvents:[NSArray arrayWithObject:event]];
}
}
static int isMiniaturized(CFTypeRef windowRef) {
@autoreleasepool {
NSWindow *window = (__bridge NSWindow *)windowRef;
return window.miniaturized ? 1 : 0;
NSEvent *event = [NSApp nextEventMatchingMask:NSEventMaskAny
untilDate:[NSDate distantFuture]
inMode:NSDefaultRunLoopMode
dequeue:YES];
[NSApp sendEvent:event];
}
}
*/
@@ -348,6 +320,7 @@ type window struct {
view C.CFTypeRef
w *callbacks
anim bool
visible bool
displayLink *displayLink
// redraw is a single entry channel for making sure only one
// display link redraw request is in flight.
@@ -355,29 +328,21 @@ type window struct {
cursor pointer.Cursor
pointerBtns pointer.Buttons
loop *eventLoop
lastMods C.NSUInteger
scale float32
config Config
keysDown map[key.Name]struct{}
// cmdKeys is for storing the current key event while
// waiting for a doCommandBySelector.
cmdKeys cmdKeys
}
type cmdKeys struct {
eventStr string
eventMods key.Modifiers
}
// launched is closed when applicationDidFinishLaunching is called.
// launched is closed after gio_initApp returns.
var launched = make(chan struct{})
// nextTopLeft is the offset to use for the next window's call to
// cascadeTopLeftFromPoint.
var nextTopLeft C.NSPoint
// mainThreadWindow is the window currently in control of the main thread.
var mainThreadWindow *window
func windowFor(h C.uintptr_t) *window {
return cgo.Handle(h).Value().(*window)
}
@@ -407,23 +372,11 @@ func (w *window) WriteClipboard(mime string, s []byte) {
}
func (w *window) updateWindowMode() {
w.scale = float32(C.getViewBackingScale(w.view))
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
w.config.Size = image.Point{
X: int(wf*w.scale + .5),
Y: int(hf*w.scale + .5),
}
w.config.Mode = Windowed
window := C.windowForView(w.view)
if window == 0 {
return
}
style := int(C.getWindowStyleMask(C.windowForView(w.view)))
switch {
case style&C.NSWindowStyleMaskFullScreen != 0:
if style&C.NSWindowStyleMaskFullScreen != 0 {
w.config.Mode = Fullscreen
case C.isWindowZoomed(window) != 0:
w.config.Mode = Maximized
} else {
w.config.Mode = Windowed
}
w.config.Decorated = style&C.NSWindowStyleMaskFullSizeContentView == 0
}
@@ -431,82 +384,102 @@ func (w *window) updateWindowMode() {
func (w *window) Configure(options []Option) {
screenScale := float32(C.getScreenBackingScale())
cfg := configFor(screenScale)
prev := w.config
w.updateWindowMode()
cnf := w.config
cnf.apply(cfg, options)
window := C.windowForView(w.view)
mask := C.getWindowStyleMask(window)
fullscreen := mask&C.NSWindowStyleMaskFullScreen != 0
switch cnf.Mode {
case Fullscreen:
if C.isWindowMiniaturized(window) != 0 {
switch prev.Mode {
case Fullscreen:
case Minimized:
C.unhideWindow(window)
}
if !fullscreen {
fallthrough
default:
w.config.Mode = Fullscreen
C.toggleFullScreen(window)
}
case Minimized:
C.hideWindow(window)
switch prev.Mode {
case Minimized, Fullscreen:
default:
w.config.Mode = Minimized
C.hideWindow(window)
}
case Maximized:
if C.isWindowMiniaturized(window) != 0 {
switch prev.Mode {
case Fullscreen:
case Minimized:
C.unhideWindow(window)
}
if fullscreen {
C.toggleFullScreen(window)
}
w.setTitle(cnf.Title)
if C.isWindowZoomed(window) == 0 {
C.zoomWindow(window)
fallthrough
default:
w.config.Mode = Maximized
w.setTitle(prev, cnf)
if C.isWindowZoomed(window) == 0 {
C.zoomWindow(window)
}
}
case Windowed:
if C.isWindowMiniaturized(window) != 0 {
C.unhideWindow(window)
}
if fullscreen {
switch prev.Mode {
case Fullscreen:
C.toggleFullScreen(window)
case Minimized:
C.unhideWindow(window)
case Maximized:
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
}
w.setTitle(cnf.Title)
w.config.Size = cnf.Size
cnf.Size = cnf.Size.Div(int(screenScale))
C.setSize(window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y))
w.config.MinSize = cnf.MinSize
cnf.MinSize = cnf.MinSize.Div(int(screenScale))
C.setMinSize(window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y))
w.config.MaxSize = cnf.MaxSize
cnf.MaxSize = cnf.MaxSize.Div(int(screenScale))
if cnf.MaxSize != (image.Point{}) {
w.config.Mode = Windowed
w.setTitle(prev, cnf)
if prev.Size != cnf.Size {
w.config.Size = cnf.Size
cnf.Size = cnf.Size.Div(int(screenScale))
C.setSize(window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y))
}
if prev.MinSize != cnf.MinSize {
w.config.MinSize = cnf.MinSize
cnf.MinSize = cnf.MinSize.Div(int(screenScale))
C.setMinSize(window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y))
}
if prev.MaxSize != cnf.MaxSize {
w.config.MaxSize = cnf.MaxSize
cnf.MaxSize = cnf.MaxSize.Div(int(screenScale))
C.setMaxSize(window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y))
}
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
if cnf.Decorated != prev.Decorated {
w.config.Decorated = cnf.Decorated
mask := C.getWindowStyleMask(window)
style := C.NSWindowStyleMask(C.NSWindowStyleMaskTitled | C.NSWindowStyleMaskResizable | C.NSWindowStyleMaskMiniaturizable | C.NSWindowStyleMaskClosable)
style = C.NSWindowStyleMaskFullSizeContentView
mask &^= style
barTrans := C.int(C.NO)
titleVis := C.NSWindowTitleVisibility(C.NSWindowTitleVisible)
if !cnf.Decorated {
mask |= style
barTrans = C.YES
titleVis = C.NSWindowTitleHidden
}
C.setWindowTitlebarAppearsTransparent(window, barTrans)
C.setWindowTitleVisibility(window, titleVis)
C.setWindowStyleMask(window, mask)
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowZoomButton, barTrans)
}
style := C.NSWindowStyleMask(C.NSWindowStyleMaskTitled | C.NSWindowStyleMaskResizable | C.NSWindowStyleMaskMiniaturizable | C.NSWindowStyleMaskClosable)
style = C.NSWindowStyleMaskFullSizeContentView
mask &^= style
barTrans := C.int(C.NO)
titleVis := C.NSWindowTitleVisibility(C.NSWindowTitleVisible)
if !cnf.Decorated {
mask |= style
barTrans = C.YES
titleVis = C.NSWindowTitleHidden
}
C.setWindowTitlebarAppearsTransparent(window, barTrans)
C.setWindowTitleVisibility(window, titleVis)
C.setWindowStyleMask(window, mask)
C.setWindowStandardButtonHidden(window, C.NSWindowCloseButton, barTrans)
C.setWindowStandardButtonHidden(window, C.NSWindowMiniaturizeButton, 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})
}
func (w *window) setTitle(title string) {
w.config.Title = title
titleC := stringToNSString(title)
defer C.CFRelease(titleC)
C.setTitle(C.windowForView(w.view), titleC)
func (w *window) setTitle(prev, cnf Config) {
if prev.Title != cnf.Title {
w.config.Title = cnf.Title
title := stringToNSString(cnf.Title)
defer C.CFRelease(title)
C.setTitle(C.windowForView(w.view), title)
}
}
func (w *window) Perform(acts system.Action) {
@@ -549,8 +522,7 @@ func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) SetAnimating(anim bool) {
w.anim = anim
window := C.windowForView(w.view)
if w.anim && window != 0 && C.isMiniaturized(window) == 0 {
if w.anim && w.visible {
w.displayLink.Start()
} else {
w.displayLink.Stop()
@@ -568,92 +540,23 @@ func (w *window) runOnMain(f func()) {
}
//export gio_onKeys
func gio_onKeys(h C.uintptr_t, event C.CFTypeRef, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
w := windowFor(h)
if w.keysDown == nil {
w.keysDown = make(map[key.Name]struct{})
}
func gio_onKeys(h C.uintptr_t, cstr C.CFTypeRef, ti C.double, mods C.NSUInteger, keyDown C.bool) {
str := nsstringToString(cstr)
kmods := convertMods(mods)
ks := key.Release
if keyDown {
ks = key.Press
w.cmdKeys.eventStr = str
w.cmdKeys.eventMods = kmods
C.interpretKeyEvents(w.view, event)
}
w := windowFor(h)
for _, k := range str {
if n, ok := convertKey(k); ok {
ke := key.Event{
w.ProcessEvent(key.Event{
Name: n,
Modifiers: kmods,
State: ks,
}
if keyDown {
w.keysDown[ke.Name] = struct{}{}
if _, isCmd := convertCommandKey(k); isCmd || kmods.Contain(key.ModCommand) {
// doCommandBySelector already processed the event.
return
}
} else {
if _, pressed := w.keysDown[n]; !pressed {
continue
}
delete(w.keysDown, n)
}
w.ProcessEvent(ke)
}
}
}
//export gio_onCommandBySelector
func gio_onCommandBySelector(h C.uintptr_t) C.bool {
w := windowFor(h)
ev := w.cmdKeys
w.cmdKeys = cmdKeys{}
handled := false
for _, k := range ev.eventStr {
n, ok := convertCommandKey(k)
if !ok && ev.eventMods.Contain(key.ModCommand) {
n, ok = convertKey(k)
}
if !ok {
continue
}
ke := key.Event{
Name: n,
Modifiers: ev.eventMods,
State: key.Press,
}
handled = w.processEvent(ke) || handled
}
return C.bool(handled)
}
//export gio_onFlagsChanged
func gio_onFlagsChanged(h C.uintptr_t, curMods C.NSUInteger) {
w := windowFor(h)
mods := []C.NSUInteger{C.NSControlKeyMask, C.NSAlternateKeyMask, C.NSShiftKeyMask, C.NSCommandKeyMask}
keys := []key.Name{key.NameCtrl, key.NameAlt, key.NameShift, key.NameCommand}
for i, mod := range mods {
wasPressed := w.lastMods&mod != 0
isPressed := curMods&mod != 0
if wasPressed != isPressed {
st := key.Release
if isPressed {
st = key.Press
}
w.ProcessEvent(key.Event{
Name: keys[i],
State: st,
})
}
}
w.lastMods = curMods
}
//export gio_onText
@@ -846,15 +749,6 @@ func gio_substringForProposedRange(h C.uintptr_t, crng C.NSRange, actual C.NSRan
//export gio_insertText
func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
w := windowFor(h)
str := nsstringToString(cstr)
// macOS IME in some cases calls insertText for command keys such as backspace
// instead of doCommandBySelector.
for _, r := range str {
if _, ok := convertCommandKey(r); ok {
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
return
}
}
state := w.w.EditorState()
rng := state.compose
if rng.Start == -1 {
@@ -866,6 +760,7 @@ func gio_insertText(h C.uintptr_t, cstr C.CFTypeRef, crng C.NSRange) {
End: state.RunesIndex(int(crng.location + crng.length)),
}
}
str := nsstringToString(cstr)
w.w.EditorReplace(rng, str)
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
start := rng.Start
@@ -906,19 +801,24 @@ func gio_firstRectForCharacterRange(h C.uintptr_t, crng C.NSRange, actual C.NSRa
}
func (w *window) draw() {
cnf := w.config
w.updateWindowMode()
if w.config != cnf {
w.ProcessEvent(ConfigEvent{Config: w.config})
}
select {
case <-w.redraw:
default:
}
w.visible = true
if w.anim {
w.SetAnimating(w.anim)
}
sz := w.config.Size
w.scale = float32(C.getViewBackingScale(w.view))
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
sz := image.Point{
X: int(wf*w.scale + .5),
Y: int(hf*w.scale + .5),
}
if sz != w.config.Size {
w.config.Size = sz
w.ProcessEvent(ConfigEvent{Config: w.config})
}
if sz.X == 0 || sz.Y == 0 {
return
}
@@ -926,7 +826,7 @@ func (w *window) draw() {
w.ProcessEvent(frameEvent{
FrameEvent: FrameEvent{
Now: time.Now(),
Size: sz,
Size: w.config.Size,
Metric: cfg,
},
Sync: true,
@@ -934,29 +834,52 @@ func (w *window) draw() {
}
func (w *window) ProcessEvent(e event.Event) {
w.processEvent(e)
}
func (w *window) processEvent(e event.Event) bool {
handled := w.w.ProcessEvent(e)
w.loop.FlushEvents()
return handled
w.w.ProcessEvent(e)
// The main thread window deliver events in Event.
if w != mainThreadWindow {
w.loop.FlushEvents()
}
}
func (w *window) Event() event.Event {
return w.loop.Event()
if !isMainThread() {
return w.loop.Event()
}
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
for {
if evt, ok := w.loop.win.nextEvent(); ok {
return evt
}
C.dispatchEvent()
gio_dispatchMainFuncs()
}
}
func (w *window) Invalidate() {
if isMainThread() {
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
}
w.loop.Invalidate()
}
func (w *window) Run(f func()) {
if isMainThread() {
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
}
w.loop.Run(f)
}
func (w *window) Frame(frame *op.Ops) {
w.loop.Frame(frame)
if !isMainThread() {
w.loop.Frame(frame)
return
}
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
w.loop.win.ProcessFrame(frame, nil)
}
func configFor(scale float32) unit.Metric {
@@ -974,6 +897,7 @@ func gio_onAttached(h C.uintptr_t, attached C.int) {
w.ProcessEvent(AppKitViewEvent{View: uintptr(w.view), Layer: uintptr(layer)})
} else {
w.ProcessEvent(AppKitViewEvent{})
w.visible = false
w.SetAnimating(w.anim)
}
}
@@ -988,31 +912,64 @@ func gio_onDestroy(h C.uintptr_t) {
w.view = 0
}
//export gio_onFinishLaunching
func gio_onFinishLaunching() {
close(launched)
//export gio_onHide
func gio_onHide(h C.uintptr_t) {
w := windowFor(h)
w.visible = false
w.SetAnimating(w.anim)
}
//export gio_onShow
func gio_onShow(h C.uintptr_t) {
w := windowFor(h)
w.draw()
}
//export gio_onFullscreen
func gio_onFullscreen(h C.uintptr_t) {
w := windowFor(h)
w.config.Mode = Fullscreen
w.ProcessEvent(ConfigEvent{Config: w.config})
}
//export gio_onWindowed
func gio_onWindowed(h C.uintptr_t) {
w := windowFor(h)
w.config.Mode = Windowed
w.ProcessEvent(ConfigEvent{Config: w.config})
}
func newWindow(win *callbacks, options []Option) {
<-launched
res := make(chan struct{})
runOnMain(func() {
w := &window{
redraw: make(chan struct{}, 1),
w: win,
w := &window{
redraw: make(chan struct{}, 1),
w: win,
}
w.loop = newEventLoop(w.w, w.wakeup)
if isMainThread() {
mainThreadWindow = w
defer func() { mainThreadWindow = nil }()
select {
case <-launched:
default:
// If we're the main thread, initialize the GUI.
C.gio_initApp()
close(launched)
}
w.loop = newEventLoop(w.w, w.wakeup)
} else {
<-launched
}
res := make(chan struct{}, 1)
runOnMain(func() {
win.SetDriver(w)
res <- struct{}{}
var cnf Config
cnf.apply(unit.Metric{}, options)
if err := w.init(cnf.CustomRenderer); err != nil {
if err := w.init(); err != nil {
w.ProcessEvent(DestroyEvent{Err: err})
return
}
window := C.gio_createWindow(w.view, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y), 0, 0, 0, 0)
window := C.gio_createWindow(w.view, 0, 0, 0, 0, 0, 0)
// Release our reference now that the NSWindow has it.
C.CFRelease(w.view)
w.updateWindowMode()
w.Configure(options)
if nextTopLeft.x == 0 && nextTopLeft.y == 0 {
// cascadeTopLeftFromPoint treats (0, 0) as a no-op,
@@ -1020,19 +977,14 @@ func newWindow(win *callbacks, options []Option) {
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
}
nextTopLeft = C.cascadeTopLeftFromPoint(window, nextTopLeft)
C.makeFirstResponder(window, w.view)
// makeKeyAndOrderFront assumes ownership of our window reference.
C.makeKeyAndOrderFront(window)
})
<-res
}
func (w *window) init(customRenderer bool) error {
presentWithTrans := 1
if customRenderer {
presentWithTrans = 0
}
view := C.gio_createView(C.int(presentWithTrans))
func (w *window) init() error {
view := C.gio_createView()
if view == 0 {
return errors.New("newOSWindow: failed to create view")
}
@@ -1045,7 +997,9 @@ func (w *window) init(customRenderer bool) error {
return
}
w.runOnMain(func() {
C.setNeedsDisplay(w.view)
if w.visible {
C.setNeedsDisplay(w.view)
}
})
})
w.displayLink = dl
@@ -1058,17 +1012,10 @@ func (w *window) init(customRenderer bool) error {
return nil
}
func osMain() {
if !isMainThread() {
panic("app.Main must run on the main goroutine")
}
C.gio_main()
}
func convertCommandKey(k rune) (key.Name, bool) {
func convertKey(k rune) (key.Name, bool) {
var n key.Name
switch k {
case '\x1b': // ASCII escape.
case 0x1b:
n = key.NameEscape
case C.NSLeftArrowFunctionKey:
n = key.NameLeftArrow
@@ -1078,36 +1025,22 @@ func convertCommandKey(k rune) (key.Name, bool) {
n = key.NameUpArrow
case C.NSDownArrowFunctionKey:
n = key.NameDownArrow
case '\r':
case 0xd:
n = key.NameReturn
case '\x03':
case 0x3:
n = key.NameEnter
case C.NSHomeFunctionKey:
n = key.NameHome
case C.NSEndFunctionKey:
n = key.NameEnd
case '\x7f', '\b':
case 0x7f:
n = key.NameDeleteBackward
case C.NSDeleteFunctionKey:
n = key.NameDeleteForward
case '\t', 0x19:
n = key.NameTab
case C.NSPageUpFunctionKey:
n = key.NamePageUp
case C.NSPageDownFunctionKey:
n = key.NamePageDown
default:
return "", false
}
return n, true
}
func convertKey(k rune) (key.Name, bool) {
if n, ok := convertCommandKey(k); ok {
return n, true
}
var n key.Name
switch k {
case C.NSF1FunctionKey:
n = key.NameF1
case C.NSF2FunctionKey:
@@ -1132,6 +1065,8 @@ func convertKey(k rune) (key.Name, bool) {
n = key.NameF11
case C.NSF12FunctionKey:
n = key.NameF12
case 0x09, 0x19:
n = key.NameTab
case 0x20:
n = key.NameSpace
default:
@@ -1161,8 +1096,14 @@ func convertMods(mods C.NSUInteger) key.Modifiers {
return kmods
}
func (w *window) wakeup() {
runOnMain(func() {
w.loop.Wakeup()
if w != mainThreadWindow {
w.loop.FlushEvents()
}
})
}
func (AppKitViewEvent) implementsViewEvent() {}
func (AppKitViewEvent) ImplementsEvent() {}
func (a AppKitViewEvent) Valid() bool {
return a != (AppKitViewEvent{})
}
+261 -48
View File
@@ -6,7 +6,7 @@
#include "_cgo_export.h"
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWithTrans);
__attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(void);
@interface GioAppDelegate : NSObject<NSApplicationDelegate>
@end
@@ -16,29 +16,28 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWi
@interface GioView : NSView <CALayerDelegate,NSTextInputClient>
@property uintptr_t handle;
@property BOOL presentWithTrans;
@end
@implementation GioWindowDelegate
- (void)windowWillMiniaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
gio_onHide(view.handle);
}
- (void)windowDidDeminiaturize:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
gio_onShow(view.handle);
}
- (void)windowWillEnterFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
gio_onFullscreen(view.handle);
}
- (void)windowWillExitFullScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
gio_onDraw(view.handle);
gio_onWindowed(view.handle);
}
- (void)windowDidChangeScreen:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
@@ -48,17 +47,13 @@ __attribute__ ((visibility ("hidden"))) CALayer *gio_layerFactory(BOOL presentWi
}
- (void)windowDidBecomeKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
if ([window firstResponder] == view) {
gio_onFocus(view.handle, 1);
}
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 1);
}
- (void)windowDidResignKey:(NSNotification *)notification {
NSWindow *window = (NSWindow *)[notification object];
GioView *view = (GioView *)window.contentView;
if ([window firstResponder] == view) {
gio_onFocus(view.handle, 0);
}
GioView *view = (GioView *)window.contentView;
gio_onFocus(view.handle, 0);
}
@end
@@ -74,6 +69,212 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
gio_onMouse(view.handle, (__bridge CFTypeRef)event, typ, event.buttonNumber, p.x, height - p.y, dx, dy, [event timestamp], [event modifierFlags]);
}
@interface GioApplication: NSApplication
@end
// Variables for tracking resizes.
static struct {
NSPoint dir;
NSEvent *lastMouseDown;
NSPoint off;
} resizeState = {};
static NSBitmapImageRep *nsImageBitmap(NSImage *img) {
NSArray<NSImageRep *> *reps = img.representations;
if ([reps count] == 0) {
return nil;
}
NSImageRep *rep = reps[0];
if (![rep isKindOfClass:[NSBitmapImageRep class]]) {
return nil;
}
return (NSBitmapImageRep *)rep;
}
static NSCursor *lookupPrivateNSCursor(SEL name) {
if (![NSCursor respondsToSelector:name]) {
return nil;
}
id obj = [NSCursor performSelector:name];
if (![obj isKindOfClass:[NSCursor class]]) {
return nil;
}
return (NSCursor *)obj;
}
static BOOL isEqualNSCursor(NSCursor *c1, SEL name2) {
NSCursor *c2 = lookupPrivateNSCursor(name2);
if (c2 == nil || !NSEqualPoints(c1.hotSpot, c2.hotSpot)) {
return NO;
}
NSImage *img1 = c1.image;
NSImage *img2 = c2.image;
if (!NSEqualSizes(img1.size, img2.size)) {
return NO;
}
NSBitmapImageRep *bit1 = nsImageBitmap(img1);
NSBitmapImageRep *bit2 = nsImageBitmap(img2);
if (bit1 == nil || bit2 == nil) {
return NO;
}
NSInteger n1 = bit1.numberOfPlanes*bit1.bytesPerPlane;
NSInteger n2 = bit1.numberOfPlanes*bit1.bytesPerPlane;
if (n1 != n2) {
return NO;
}
if (memcmp(bit1.bitmapData, bit2.bitmapData, n1) != 0) {
return NO;
}
return YES;
}
@implementation GioApplication
- (NSEvent *)nextEventMatchingMask:(NSEventMask)mask
untilDate:(NSDate *)expiration
inMode:(NSRunLoopMode)mode
dequeue:(BOOL)deqFlag {
if ([mode isEqualToString:NSEventTrackingRunLoopMode]) {
NSEvent *l = resizeState.lastMouseDown;
if (l != nil) {
//lastMouseDown = nil;
NSCursor *cur = [NSCursor currentSystemCursor];
NSPoint dir = {};
NSPoint off = {};
NSSize wsz = [l window].frame.size;
NSPoint center = NSMakePoint(wsz.width/2, wsz.height/2);
NSPoint p = [l locationInWindow];
if (p.x >= center.x) {
dir.x = 1;
off.x = p.x - wsz.width;
} else {
dir.x = -1;
off.x = p.x;
}
if (p.y >= center.y) {
dir.y = 1;
off.y = p.y - wsz.height;
} else {
dir.y = -1;
off.y = p.y;
}
// The button down coordinate distinguish the four quadrants. Use the
// cursor image to determine the precise direction.
SEL nw = @selector(_windowResizeNorthWestCursor);
SEL n = @selector(_windowResizeNorthCursor);
SEL ne = @selector(_windowResizeNorthEastCursor);
SEL e = @selector(_windowResizeEastCursor);
SEL se = @selector(_windowResizeSouthEastCursor);
SEL s = @selector(_windowResizeSouthCursor);
SEL sw = @selector(_windowResizeSouthWestCursor);
SEL w = @selector(_windowResizeWestCursor);
SEL ns = @selector(_windowResizeNorthSouthCursor);
SEL ew = @selector(_windowResizeEastWestCursor);
SEL nwse = @selector(_windowResizeNorthWestSouthEastCursor);
SEL nesw = @selector(_windowResizeNorthEastSouthWestCursor);
BOOL match = YES;
if (dir.x != 0 && (isEqualNSCursor(cur, ew) || isEqualNSCursor(cur, w) || isEqualNSCursor(cur, e))) {
dir.y = 0;
}
if (dir.y != 0 && (isEqualNSCursor(cur, ns) || isEqualNSCursor(cur, s) || isEqualNSCursor(cur, n))) {
dir.x = 0;
}
// If none of the cursors matched, we may deduce that the resize
// direction is one of the corners. However, to ensure that at least
// one cursor matches, check the corner cursors.
if (dir.x == 1 && dir.y == 1) {
if (!isEqualNSCursor(cur, nesw) && !isEqualNSCursor(cur, sw)) {
dir = NSZeroPoint;
}
} else if (dir.x == 1 && dir.y == -1) {
if (!isEqualNSCursor(cur, nwse) && !isEqualNSCursor(cur, nw)) {
dir = NSZeroPoint;
}
} else if (dir.x == -1 && dir.y == 1) {
if (!isEqualNSCursor(cur, nwse) && !isEqualNSCursor(cur, se)) {
dir = NSZeroPoint;
}
} else if (dir.x == -1 && dir.y == -1) {
if (!isEqualNSCursor(cur, nesw) && !isEqualNSCursor(cur, ne)) {
dir = NSZeroPoint;
}
}
if (!NSEqualPoints(dir, NSZeroPoint)) {
NSEvent *cancel = [NSEvent mouseEventWithType:NSEventTypeLeftMouseUp
location:l.locationInWindow
modifierFlags:l.modifierFlags
timestamp:l.timestamp
windowNumber:l.windowNumber
context:l.context
eventNumber:l.eventNumber
clickCount:l.clickCount
pressure:l.pressure];
resizeState.off = off;
resizeState.dir = dir;
return cancel;
}
}
}
return [super nextEventMatchingMask:mask untilDate:expiration inMode:mode dequeue:deqFlag];
}
@end
@interface GioWindow: NSWindow
@end
@implementation GioWindow
- (void)sendEvent:(NSEvent *)evt {
if (evt.type == NSEventTypeLeftMouseDown) {
resizeState.lastMouseDown = evt;
}
NSPoint dir = resizeState.dir;
if (NSEqualPoints(dir, NSZeroPoint)) {
[super sendEvent:evt];
return;
}
switch (evt.type) {
default:
return;
case NSEventTypeLeftMouseUp:
resizeState.dir = NSZeroPoint;
resizeState.lastMouseDown = nil;
return;
case NSEventTypeLeftMouseDragged:
// Ok to proceed.
break;
}
NSPoint loc = evt.locationInWindow;
NSPoint off = resizeState.off;
loc.x -= off.x;
loc.y -= off.y;
NSRect frame = [self frame];
NSSize min = [self minSize];
NSSize max = [self maxSize];
CGFloat width = frame.size.width;
if (dir.x > 0) {
width = loc.x;
} else if (dir.x < 0) {
width -= loc.x;
}
width = MIN(max.width, MAX(min.width, width));
if (dir.x < 0) {
frame.origin.x += frame.size.width - width;
}
frame.size.width = width;
CGFloat height = frame.size.height;
if (dir.y > 0) {
height = loc.y;
} else if (dir.y < 0) {
height -= loc.y;
}
height = MIN(max.height, MAX(min.height, height));
if (dir.y < 0) {
frame.origin.y += frame.size.height - height;
}
frame.size.height = height;
[self setFrame:frame display:YES animate:NO];
}
@end
@implementation GioView
- (void)setFrameSize:(NSSize)newSize {
[super setFrameSize:newSize];
@@ -89,7 +290,7 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
gio_onDraw(self.handle);
}
- (CALayer *)makeBackingLayer {
CALayer *layer = gio_layerFactory(self.presentWithTrans);
CALayer *layer = gio_layerFactory();
layer.delegate = self;
return layer;
}
@@ -132,24 +333,20 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
handleMouse(self, event, MOUSE_SCROLL, dx, dy);
}
- (void)keyDown:(NSEvent *)event {
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys(self.handle, (__bridge CFTypeRef)event, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
}
- (void)flagsChanged:(NSEvent *)event {
[self interpretKeyEvents:[NSArray arrayWithObject:event]];
gio_onFlagsChanged(self.handle, [event modifierFlags]);
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], true);
}
- (void)keyUp:(NSEvent *)event {
NSString *keys = [event charactersIgnoringModifiers];
gio_onKeys(self.handle, (__bridge CFTypeRef)event, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
gio_onKeys(self.handle, (__bridge CFTypeRef)keys, [event timestamp], [event modifierFlags], false);
}
- (void)insertText:(id)string {
gio_onText(self.handle, (__bridge CFTypeRef)string);
}
- (void)doCommandBySelector:(SEL)action {
if (!gio_onCommandBySelector(self.handle)) {
[super doCommandBySelector:action];
}
- (void)doCommandBySelector:(SEL)sel {
// Don't pass commands up the responder chain.
// They will end up in a beep.
}
- (BOOL)hasMarkedText {
int res = gio_hasMarkedText(self.handle);
@@ -205,22 +402,14 @@ static void handleMouse(GioView *view, NSEvent *event, int typ, CGFloat dx, CGFl
return [[self window] convertRectToScreen:r];
}
- (void)applicationWillUnhide:(NSNotification *)notification {
gio_onDraw(self.handle);
gio_onShow(self.handle);
}
- (void)applicationDidHide:(NSNotification *)notification {
gio_onDraw(self.handle);
gio_onHide(self.handle);
}
- (void)dealloc {
gio_onDestroy(self.handle);
}
- (BOOL) becomeFirstResponder {
gio_onFocus(self.handle, 1);
return [super becomeFirstResponder];
}
- (BOOL) resignFirstResponder {
gio_onFocus(self.handle, 0);
return [super resignFirstResponder];
}
@end
// Delegates are weakly referenced from their peers. Nothing
@@ -271,14 +460,11 @@ void gio_showCursor() {
// some cursors are not public, this tries to use a private cursor
// and uses fallback when the use of private cursor fails.
static void trySetPrivateCursor(SEL cursorName, NSCursor* fallback) {
if ([NSCursor respondsToSelector:cursorName]) {
id object = [NSCursor performSelector:cursorName];
if ([object isKindOfClass:[NSCursor class]]) {
[(NSCursor*)object set];
return;
}
NSCursor *cur = lookupPrivateNSCursor(cursorName);
if (cur == nil) {
cur = fallback;
}
[fallback set];
[cur set];
}
void gio_setCursor(NSUInteger curID) {
@@ -377,7 +563,7 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
NSMiniaturizableWindowMask |
NSClosableWindowMask;
NSWindow* window = [[NSWindow alloc] initWithContentRect:rect
GioWindow* window = [[GioWindow alloc] initWithContentRect:rect
styleMask:styleMask
backing:NSBackingStoreBuffered
defer:NO];
@@ -390,16 +576,16 @@ CFTypeRef gio_createWindow(CFTypeRef viewRef, CGFloat width, CGFloat height, CGF
[window setAcceptsMouseMovedEvents:YES];
NSView *view = (__bridge NSView *)viewRef;
[window setContentView:view];
[window makeFirstResponder:view];
window.delegate = globalWindowDel;
return (__bridge_retained CFTypeRef)window;
}
}
CFTypeRef gio_createView(int presentWithTrans) {
CFTypeRef gio_createView(void) {
@autoreleasepool {
NSRect frame = NSMakeRect(0, 0, 0, 0);
GioView* view = [[GioView alloc] initWithFrame:frame];
view.presentWithTrans = presentWithTrans ? YES : NO;
view.wantsLayer = YES;
view.layerContentsRedrawPolicy = NSViewLayerContentsRedrawDuringViewResize;
@@ -426,13 +612,24 @@ void gio_viewSetHandle(CFTypeRef viewRef, uintptr_t handle) {
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[NSApp activateIgnoringOtherApps:YES];
gio_onFinishLaunching();
// Force the [NSApp run] call to return.
[NSApp stop:nil];
NSEvent *dummy = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSZeroPoint
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:dummy atStart:YES];
}
@end
void gio_main() {
void gio_initApp() {
@autoreleasepool {
[NSApplication sharedApplication];
[GioApplication sharedApplication];
GioAppDelegate *del = [[GioAppDelegate alloc] init];
[NSApp setDelegate:del];
@@ -454,6 +651,22 @@ void gio_main() {
globalWindowDel = [[GioWindowDelegate alloc] init];
// Runs until stopped by applicationDidFinishLaunching.
[NSApp run];
}
}
void gio_wakeupMainThread(void) {
@autoreleasepool {
NSEvent *dummy = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSZeroPoint
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:dummy atStart:YES];
}
}
+24 -10
View File
@@ -9,6 +9,7 @@ import (
"errors"
"unsafe"
"gioui.org/io/event"
"gioui.org/io/pointer"
)
@@ -21,9 +22,6 @@ type X11ViewEvent struct {
func (X11ViewEvent) implementsViewEvent() {}
func (X11ViewEvent) ImplementsEvent() {}
func (x X11ViewEvent) Valid() bool {
return x != (X11ViewEvent{})
}
type WaylandViewEvent struct {
// Display is the *wl_display returned by wl_display_connect.
@@ -34,13 +32,6 @@ type WaylandViewEvent struct {
func (WaylandViewEvent) implementsViewEvent() {}
func (WaylandViewEvent) ImplementsEvent() {}
func (w WaylandViewEvent) Valid() bool {
return w != (WaylandViewEvent{})
}
func osMain() {
select {}
}
type windowDriver func(*callbacks, []Option) error
@@ -62,12 +53,35 @@ func newWindow(window *callbacks, options []Option) {
errFirst = err
}
}
window.SetDriver(&dummyDriver{
win: window,
wakeups: make(chan event.Event, 1),
})
if errFirst == nil {
errFirst = errors.New("app: no window driver available")
}
window.ProcessEvent(DestroyEvent{Err: errFirst})
}
type dummyDriver struct {
win *callbacks
wakeups chan event.Event
}
func (d *dummyDriver) Event() event.Event {
if e, ok := d.win.nextEvent(); ok {
return e
}
return <-d.wakeups
}
func (d *dummyDriver) Invalidate() {
select {
case d.wakeups <- wakeupEvent{}:
default:
}
}
// xCursor contains mapping from pointer.Cursor to XCursor.
var xCursor = [...]string{
pointer.CursorDefault: "left_ptr",
+24 -20
View File
@@ -216,7 +216,9 @@ 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
}
type poller struct {
@@ -554,7 +556,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.closing = true
w.close(nil)
}
//export gio_onToplevelConfigure
@@ -1137,7 +1139,7 @@ func (w *window) Perform(actions system.Action) {
walkActions(actions, func(action system.Action) {
switch action {
case system.ActionClose:
w.closing = true
w.close(nil)
}
})
}
@@ -1364,9 +1366,6 @@ 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.disp.destroy()
w.disp = nil
}
func (w *window) dispatch() {
@@ -1375,7 +1374,7 @@ func (w *window) dispatch() {
w.w.Invalidate()
return
}
if err := w.disp.dispatch(); err != nil || w.closing {
if err := w.disp.dispatch(); err != nil {
w.close(err)
return
}
@@ -1400,6 +1399,13 @@ 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
}
}
@@ -1410,7 +1416,11 @@ func (w *window) Invalidate() {
default:
return
}
w.disp.wakeup()
w.invMu.Lock()
defer w.invMu.Unlock()
if w.disp != nil {
w.disp.wakeup()
}
}
func (w *window) Run(f func()) {
@@ -1505,10 +1515,6 @@ 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)
}
@@ -1633,14 +1639,6 @@ func (w *window) flushScroll() {
if total == (f32.Point{}) {
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{
Kind: pointer.Scroll,
Source: pointer.Mouse,
@@ -1650,6 +1648,12 @@ func (w *window) flushScroll() {
Time: w.scroll.time,
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) {
+59 -63
View File
@@ -46,13 +46,17 @@ type window struct {
cursorIn bool
cursor syscall.Handle
// placement saves the previous window position when in full screen mode.
placement *windows.WindowPlacement
animating bool
borderSize image.Point
config Config
// frameDims stores the last seen window frame width and height.
frameDims image.Point
loop *eventLoop
loop *eventLoop
// invMu avoids the race between destroying the window and Invalidate.
invMu sync.Mutex
}
const _WM_WAKEUP = windows.WM_USER + iota
@@ -81,10 +85,6 @@ var resources struct {
cursor syscall.Handle
}
func osMain() {
select {}
}
func newWindow(win *callbacks, options []Option) {
done := make(chan struct{})
go func() {
@@ -107,8 +107,8 @@ func newWindow(win *callbacks, options []Option) {
}
winMap.Store(w.hwnd, w)
defer winMap.Delete(w.hwnd)
w.Configure(options)
w.ProcessEvent(Win32ViewEvent{HWND: uintptr(w.hwnd)})
w.Configure(options)
windows.SetForegroundWindow(w.hwnd)
windows.SetFocus(w.hwnd)
// Since the window class for the cursor is null,
@@ -184,39 +184,21 @@ func (w *window) init() error {
return nil
}
// update handles changes done by the user, and updates the configuration.
// update() handles changes done by the user, and updates the configuration.
// It reads the window style and size/position and updates w.config.
// If anything has changed it emits a ConfigEvent to notify the application.
func (w *window) update() {
p := windows.GetWindowPlacement(w.hwnd)
if !p.IsMinimized() {
r := windows.GetWindowRect(w.hwnd)
cr := windows.GetClientRect(w.hwnd)
w.config.Size = image.Point{
X: int(cr.Right - cr.Left),
Y: int(cr.Bottom - cr.Top),
}
w.frameDims = image.Point{
X: int(r.Right - r.Left),
Y: int(r.Bottom - r.Top),
}.Sub(w.config.Size)
cr := windows.GetClientRect(w.hwnd)
w.config.Size = image.Point{
X: int(cr.Right - cr.Left),
Y: int(cr.Bottom - cr.Top),
}
w.borderSize = image.Pt(
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
)
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
switch {
case p.IsMaximized() && style&windows.WS_OVERLAPPEDWINDOW != 0:
w.config.Mode = Maximized
case p.IsMaximized():
w.config.Mode = Fullscreen
default:
w.config.Mode = Windowed
}
w.ProcessEvent(ConfigEvent{Config: w.config})
w.draw(true)
}
func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr {
@@ -314,15 +296,15 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
case windows.WM_DESTROY:
w.ProcessEvent(Win32ViewEvent{})
w.ProcessEvent(DestroyEvent{})
w.w = nil
if w.hdc != 0 {
windows.ReleaseDC(w.hdc)
w.hdc = 0
}
w.invMu.Lock()
// The system destroys the HWND for us.
w.hwnd = 0
w.invMu.Unlock()
windows.PostQuitMessage(0)
return 0
case windows.WM_NCCALCSIZE:
if w.config.Decorated {
// Let Windows handle decorations.
@@ -347,31 +329,37 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
return 0
case windows.WM_PAINT:
w.draw(true)
case windows.WM_STYLECHANGED:
w.update()
case windows.WM_WINDOWPOSCHANGED:
w.update()
case windows.WM_SIZE:
w.update()
switch wParam {
case windows.SIZE_MINIMIZED:
w.config.Mode = Minimized
case windows.SIZE_MAXIMIZED:
w.config.Mode = Maximized
case windows.SIZE_RESTORED:
if w.config.Mode != Fullscreen {
w.config.Mode = Windowed
}
}
case windows.WM_GETMINMAXINFO:
mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam))
var frameDims image.Point
var bw, bh int32
if w.config.Decorated {
frameDims = w.frameDims
r := windows.GetWindowRect(w.hwnd)
cr := windows.GetClientRect(w.hwnd)
bw = r.Right - r.Left - (cr.Right - cr.Left)
bh = r.Bottom - r.Top - (cr.Bottom - cr.Top)
}
if p := w.config.MinSize; p.X > 0 || p.Y > 0 {
p = p.Add(frameDims)
mm.PtMinTrackSize = windows.Point{
X: int32(p.X),
Y: int32(p.Y),
X: int32(p.X) + bw,
Y: int32(p.Y) + bh,
}
}
if p := w.config.MaxSize; p.X > 0 || p.Y > 0 {
p = p.Add(frameDims)
mm.PtMaxTrackSize = windows.Point{
X: int32(p.X),
Y: int32(p.Y),
X: int32(p.X) + bw,
Y: int32(p.Y) + bh,
}
}
return 0
@@ -463,6 +451,9 @@ func getModifiers() key.Modifiers {
// hitTest returns the non-client area hit by the point, needed to
// process WM_NCHITTEST.
func (w *window) hitTest(x, y int) uintptr {
if w.config.Mode == Fullscreen {
return windows.HTCLIENT
}
if w.config.Mode != Windowed {
// Only windowed mode should allow resizing.
return windows.HTCLIENT
@@ -572,8 +563,7 @@ func (w *window) runLoop() {
loop:
for {
anim := w.animating
p := windows.GetWindowPlacement(w.hwnd)
if anim && !p.IsMinimized() && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) {
if anim && !windows.PeekMessage(msg, 0, 0, 0, windows.PM_NOREMOVE) {
w.draw(false)
continue
}
@@ -626,6 +616,13 @@ func (w *window) Frame(frame *op.Ops) {
}
func (w *window) wakeup() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.hwnd == 0 {
w.loop.Wakeup()
w.loop.FlushEvents()
return
}
if err := windows.PostMessage(w.hwnd, _WM_WAKEUP, 0, 0); err != nil {
panic(err)
}
@@ -696,13 +693,8 @@ func (w *window) readClipboard() error {
func (w *window) Configure(options []Option) {
dpi := windows.GetSystemDPI()
metric := configForDPI(dpi)
cnf := w.config
cnf.apply(metric, options)
w.config.Title = cnf.Title
w.config.Decorated = cnf.Decorated
w.config.MinSize = cnf.MinSize
w.config.MaxSize = cnf.MaxSize
windows.SetWindowText(w.hwnd, cnf.Title)
w.config.apply(metric, options)
windows.SetWindowText(w.hwnd, w.config.Title)
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
var showMode int32
@@ -710,7 +702,7 @@ func (w *window) Configure(options []Option) {
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
style &^= winStyle
switch cnf.Mode {
switch w.config.Mode {
case Minimized:
style |= winStyle
swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE
@@ -725,13 +717,13 @@ func (w *window) Configure(options []Option) {
style |= winStyle
showMode = windows.SW_SHOWNORMAL
// Get target for client area size.
width = int32(cnf.Size.X)
height = int32(cnf.Size.Y)
width = int32(w.config.Size.X)
height = int32(w.config.Size.Y)
// Get the current window size and position.
wr := windows.GetWindowRect(w.hwnd)
x = wr.Left
y = wr.Top
if cnf.Decorated {
if w.config.Decorated {
// Compute client size and position. Note that the client size is
// equal to the window size when we are in control of decorations.
r := windows.Rect{
@@ -741,18 +733,25 @@ func (w *window) Configure(options []Option) {
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
width = r.Right - r.Left
height = r.Bottom - r.Top
} else {
}
if !w.config.Decorated {
// Enable drop shadows when we draw decorations.
windows.DwmExtendFrameIntoClientArea(w.hwnd, windows.Margins{-1, -1, -1, -1})
}
case Fullscreen:
swpStyle |= windows.SWP_NOMOVE | windows.SWP_NOSIZE
mi := windows.GetMonitorInfo(w.hwnd)
x, y = mi.Monitor.Left, mi.Monitor.Top
width = mi.Monitor.Right - mi.Monitor.Left
height = mi.Monitor.Bottom - mi.Monitor.Top
showMode = windows.SW_SHOWMAXIMIZED
}
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style)
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle)
windows.ShowWindow(w.hwnd, showMode)
w.update()
}
func (w *window) WriteClipboard(mime string, s []byte) {
@@ -990,6 +989,3 @@ func configForDPI(dpi int) unit.Metric {
func (Win32ViewEvent) implementsViewEvent() {}
func (Win32ViewEvent) ImplementsEvent() {}
func (w Win32ViewEvent) Valid() bool {
return w != (Win32ViewEvent{})
}
+14 -10
View File
@@ -113,6 +113,9 @@ type x11Window struct {
wakeups chan struct{}
handler x11EventHandler
buf [100]byte
// invMy avoids the race between destroy and Invalidate.
invMu sync.Mutex
}
var (
@@ -386,7 +389,6 @@ 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 {
@@ -396,6 +398,9 @@ func (w *x11Window) Event() event.Event {
w.dispatch()
continue
}
if _, destroy := evt.(DestroyEvent); destroy {
w.destroy()
}
return evt
}
}
@@ -413,6 +418,11 @@ func (w *x11Window) Invalidate() {
case w.wakeups <- struct{}{}:
default:
}
w.invMu.Lock()
defer w.invMu.Unlock()
if w.x == nil {
return
}
if _, err := syscall.Write(w.notify.write, x11OneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
@@ -454,12 +464,7 @@ 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.
syn = w.handler.handleEvents()
if w.x == nil {
// handleEvents received a close request and destroyed the window.
return
}
if !syn {
if syn = w.handler.handleEvents(); !syn {
anim = w.animating
if !anim {
// Clear poll events.
@@ -471,9 +476,6 @@ 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:
}
}
@@ -501,6 +503,8 @@ func (w *x11Window) dispatch() {
}
func (w *x11Window) destroy() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.notify.write != 0 {
syscall.Close(w.notify.write)
w.notify.write = 0
+1 -1
View File
@@ -25,6 +25,6 @@ func runMain() {
// Indirect call, since the linker does not know the address of main when
// laying down this package.
fn := mainMain
fn()
go fn()
})
}
+1 -1
View File
@@ -175,7 +175,7 @@ func (c *vkContext) refresh(surf vk.Surface, width, height int) error {
if err != nil {
return err
}
minExt, maxExt := vk.SurfaceCapabilitiesMinExtent(caps), vk.SurfaceCapabilitiesMaxExtent(caps)
minExt, maxExt := caps.MinExtent(), caps.MaxExtent()
if width < minExt.X || maxExt.X < width || height < minExt.Y || maxExt.Y < height {
return errOutOfDate
}
+81 -130
View File
@@ -7,6 +7,7 @@ import (
"fmt"
"image"
"image/color"
"reflect"
"runtime"
"sync"
"time"
@@ -35,15 +36,14 @@ type Option func(unit.Metric, *Config)
// Window represents an operating system window.
//
// 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.
// 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.
//
// 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
initialActions []system.Action
ctx context
gpu gpu.GPU
// timer tracks the delayed invalidate goroutine.
@@ -89,18 +89,13 @@ type Window struct {
}
imeState editorState
driver driver
// gpuErr tracks the GPU error that is to be reported when
// the window is closed.
gpuErr error
// invMu protects mayInvalidate.
invMu sync.Mutex
mayInvalidate bool
// 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 recent frame event.
// frame tracks the most recently frame event.
lastFrame struct {
sync bool
size image.Point
@@ -110,12 +105,11 @@ type Window struct {
}
type eventSummary struct {
wakeup bool
cfg *ConfigEvent
view *ViewEvent
frame *frameEvent
framePending bool
destroy *DestroyEvent
wakeup bool
cfg *ConfigEvent
view *ViewEvent
frame *frameEvent
destroy *DestroyEvent
}
type callbacks struct {
@@ -222,7 +216,6 @@ func (w *Window) frame(frame *op.Ops, viewport image.Point) error {
}
func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
w.coalesced.framePending = false
wrapper := &w.decorations.Ops
off := op.Offset(w.lastFrame.off).Push(wrapper)
ops.AddCall(&wrapper.Internal, &frame.Internal, ops.PC{}, ops.PCFor(&frame.Internal))
@@ -230,8 +223,7 @@ func (w *Window) processFrame(frame *op.Ops, ack chan<- struct{}) {
w.lastFrame.deco.Add(wrapper)
if err := w.validateAndProcess(w.lastFrame.size, w.lastFrame.sync, wrapper, ack); err != nil {
w.destroyGPU()
w.gpuErr = err
w.driver.Perform(system.ActionClose)
w.driver.ProcessEvent(DestroyEvent{Err: err})
return
}
w.updateState()
@@ -281,12 +273,8 @@ func (w *Window) updateState() {
//
// Invalidate is safe for concurrent use.
func (w *Window) Invalidate() {
w.invMu.Lock()
defer w.invMu.Unlock()
if w.mayInvalidate {
w.mayInvalidate = false
w.driver.Invalidate()
}
w.init()
w.basic.Invalidate()
}
// Option applies the options to the window. The options are hints; the platform is
@@ -295,10 +283,7 @@ func (w *Window) Option(opts ...Option) {
if len(opts) == 0 {
return
}
if w.driver == nil {
w.initialOpts = append(w.initialOpts, opts...)
return
}
w.init(opts...)
w.Run(func() {
cnf := Config{Decorated: w.decorations.enabled}
for _, opt := range opts {
@@ -317,14 +302,16 @@ 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. If the window has not yet been created,
// Run calls f directly.
// return or the window to close. Run is guaranteed not to deadlock if it is
// invoked during the handling of a [ViewEvent], [FrameEvent],
// [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
// [CustomRenderer] is a notable exception.
func (w *Window) Run(f func()) {
w.init()
if w.driver == nil {
f()
return
}
done := make(chan struct{})
@@ -390,13 +377,11 @@ func (w *Window) setNextFrame(at time.Time) {
}
}
func (c *callbacks) SetDriver(d driver) {
if d == nil {
panic("nil driver")
func (c *callbacks) SetDriver(d basicDriver) {
c.w.basic = d
if d, ok := d.(driver); ok {
c.w.driver = d
}
c.w.invMu.Lock()
defer c.w.invMu.Unlock()
c.w.driver = d
}
func (c *callbacks) ProcessFrame(frame *op.Ops, ack chan<- struct{}) {
@@ -563,20 +548,10 @@ func (c *callbacks) Invalidate() {
}
func (c *callbacks) nextEvent() (event.Event, bool) {
return c.w.nextEvent()
}
func (w *Window) nextEvent() (event.Event, bool) {
s := &w.coalesced
defer func() {
// Every event counts as a wakeup.
s.wakeup = false
}()
s := &c.w.coalesced
// Every event counts as a wakeup.
defer func() { s.wakeup = false }()
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:
e := *s.view
s.view = nil
@@ -593,14 +568,10 @@ func (w *Window) nextEvent() (event.Event, bool) {
case s.frame != nil:
e := *s.frame
s.frame = nil
s.framePending = true
return e.FrameEvent, true
case s.wakeup:
return wakeupEvent{}, true
}
w.invMu.Lock()
defer w.invMu.Unlock()
w.mayInvalidate = w.driver != nil
return nil, false
}
@@ -643,21 +614,15 @@ func (w *Window) processEvent(e event.Event) bool {
e2.Size = e2.Size.Sub(offset)
w.coalesced.frame = &e2
case DestroyEvent:
if w.gpuErr != nil {
e2.Err = w.gpuErr
}
w.destroyGPU()
w.invMu.Lock()
w.mayInvalidate = false
w.driver = nil
w.invMu.Unlock()
if q := w.timer.quit; q != nil {
q <- struct{}{}
<-q
}
w.coalesced.destroy = &e2
case ViewEvent:
if !e2.Valid() && w.gpu != nil {
if reflect.ValueOf(e2).IsZero() && w.gpu != nil {
w.ctx.Lock()
w.gpu.Release()
w.gpu = nil
@@ -665,7 +630,6 @@ func (w *Window) processEvent(e event.Event) bool {
}
w.coalesced.view = &e2
case ConfigEvent:
w.decorations.Decorations.Maximized = e2.Config.Mode == Maximized
wasFocused := w.decorations.Config.Focused
w.decorations.Config = e2.Config
e2.Config = w.effectiveConfig()
@@ -719,61 +683,55 @@ 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. The window is created
// and shown the first time Event is called.
// [FrameEvent], or until [Invalidate] is called.
//
// Note: if more than one Window is active, at least one must have
// its Event called from the main goroutine that runs the main
// function. This is necessary because some operating system GUI
// implementations require control of the main thread.
// For this reason, it is allowed to call Event even after a
// DestroyEvent has been received.
func (w *Window) Event() event.Event {
if w.driver == nil {
w.init()
}
if w.driver == nil {
e, ok := w.nextEvent()
if !ok {
panic("window initialization failed without a DestroyEvent")
}
return e
}
return w.driver.Event()
w.init()
return w.basic.Event()
}
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)
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)
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)
for _, acts := range w.initialActions {
w.Perform(acts)
}
w.initialActions = nil
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() {
@@ -811,6 +769,7 @@ func (w *Window) decorate(e FrameEvent, o *op.Ops) image.Point {
default:
panic(fmt.Errorf("unknown WindowMode %v", m))
}
deco.Perform(actions)
gtx := layout.Context{
Ops: o,
Now: e.Now,
@@ -820,12 +779,8 @@ 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))
if len(opts) > 0 {
w.driver.Configure(opts)
}
if acts != 0 {
w.driver.Perform(acts)
}
w.driver.Configure(opts)
w.driver.Perform(acts)
style.Layout(gtx)
// Offset to place the frame content below the decorations.
decoHeight := gtx.Dp(w.decorations.Config.decoHeight)
@@ -872,10 +827,6 @@ func (w *Window) Perform(actions system.Action) {
if acts == 0 {
return
}
if w.driver == nil {
w.initialActions = append(w.initialActions, acts)
return
}
w.Run(func() {
w.driver.Perform(actions)
})
Generated
+31 -15
View File
@@ -9,11 +9,11 @@
]
},
"locked": {
"lastModified": 1733430059,
"narHash": "sha256-o3O5tjrMMebRLuHQt7BbEw3jZgWRW5vnOptNXv8WdO4=",
"lastModified": 1701721028,
"narHash": "sha256-2z4YrdHPLoMZNWR1MPOjNZMqPg057i1eZXaYI6RTahQ=",
"owner": "tadfisher",
"repo": "android-nixpkgs",
"rev": "d2f3c1ea99c0bea9d28a0e59daeb482f50d4cd35",
"rev": "c923f9ec0f4dd0d7dc725dc5b73fbf03658e50dd",
"type": "github"
},
"original": {
@@ -27,14 +27,15 @@
"nixpkgs": [
"android",
"nixpkgs"
]
],
"systems": "systems"
},
"locked": {
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"lastModified": 1701697687,
"narHash": "sha256-dLLE5wQBVv+pIb4bWmKFSw2DvLVyuEk0F7ng6hpZPSU=",
"owner": "numtide",
"repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"rev": "c3bd77911391eb1638af6ce773de86da57ee6df5",
"type": "github"
},
"original": {
@@ -45,14 +46,14 @@
},
"flake-utils": {
"inputs": {
"systems": "systems"
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -63,16 +64,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1733261153,
"narHash": "sha256-eq51hyiaIwtWo19fPEeE0Zr2s83DYMKJoukNLgGGpek=",
"lastModified": 1701282334,
"narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b681065d0919f7eb5309a93cea2cfa84dec9aa88",
"rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11",
"ref": "23.11",
"repo": "nixpkgs",
"type": "github"
}
@@ -97,6 +98,21 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+7 -1
View File
@@ -3,7 +3,7 @@
description = "Gio build environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
nixpkgs.url = "github:NixOS/nixpkgs/23.11";
android.url = "github:tadfisher/android-nixpkgs";
android.inputs.nixpkgs.follows = "nixpkgs";
};
@@ -47,6 +47,12 @@
xorg.libXfixes
libGL
pkg-config
] else if stdenv.isDarwin then [
darwin.apple_sdk_11_0.frameworks.Foundation
darwin.apple_sdk_11_0.frameworks.Metal
darwin.apple_sdk_11_0.frameworks.QuartzCore
darwin.apple_sdk_11_0.frameworks.AppKit
darwin.apple_sdk_11_0.MacOSX-SDK
] else [ ]);
} // (if stdenv.isLinux then {
LD_LIBRARY_PATH = "${vulkan-loader}/lib";
+1 -1
View File
@@ -34,7 +34,7 @@ type Font struct {
// Face is an opaque handle to a typeface. The concrete implementation depends
// upon the kind of font and shaper in use.
type Face interface {
Face() *font.Face
Face() font.Face
}
// Typeface identifies a list of font families to attempt to use for displaying
+43 -41
View File
@@ -16,21 +16,23 @@ import (
_ "image/png"
giofont "gioui.org/font"
fontapi "github.com/go-text/typesetting/font"
"github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/font"
fontapi "github.com/go-text/typesetting/opentype/api/font"
"github.com/go-text/typesetting/opentype/api/metadata"
"github.com/go-text/typesetting/opentype/loader"
)
// Face is a thread-safe representation of a loaded font. For efficiency, applications
// should construct a face for any given font file once, reusing it across different
// text shapers.
type Face struct {
face *fontapi.Font
face font.Font
font giofont.Font
}
// Parse constructs a Face from source bytes.
func Parse(src []byte) (Face, error) {
ld, err := opentype.NewLoader(bytes.NewReader(src))
ld, err := loader.NewLoader(bytes.NewReader(src))
if err != nil {
return Face{}, err
}
@@ -47,11 +49,11 @@ func Parse(src []byte) (Face, error) {
// ParseCollection parse an Opentype font file, with support for collections.
// Single font files are supported, returning a slice with length 1.
// The returned fonts are automatically wrapped in a text.FontFace with
// inferred font font.
// inferred font metadata.
// BUG(whereswaldon): the only Variant that can be detected automatically is
// "Mono".
func ParseCollection(src []byte) ([]giofont.FontFace, error) {
lds, err := opentype.NewLoaders(bytes.NewReader(src))
lds, err := loader.NewLoaders(bytes.NewReader(src))
if err != nil {
return nil, err
}
@@ -74,7 +76,7 @@ func ParseCollection(src []byte) ([]giofont.FontFace, error) {
return out, nil
}
func DescriptionToFont(md fontapi.Description) giofont.Font {
func DescriptionToFont(md metadata.Description) giofont.Font {
return giofont.Font{
Typeface: giofont.Typeface(md.Family),
Style: gioStyle(md.Aspect.Style),
@@ -82,30 +84,30 @@ func DescriptionToFont(md fontapi.Description) giofont.Font {
}
}
func FontToDescription(font giofont.Font) fontapi.Description {
return fontapi.Description{
func FontToDescription(font giofont.Font) metadata.Description {
return metadata.Description{
Family: string(font.Typeface),
Aspect: fontapi.Aspect{
Aspect: metadata.Aspect{
Style: mdStyle(font.Style),
Weight: mdWeight(font.Weight),
},
}
}
// parseLoader parses the contents of the loader into a face and its font.
func parseLoader(ld *opentype.Loader) (*fontapi.Font, giofont.Font, error) {
// parseLoader parses the contents of the loader into a face and its metadata.
func parseLoader(ld *loader.Loader) (font.Font, giofont.Font, error) {
ft, err := fontapi.NewFont(ld)
if err != nil {
return nil, giofont.Font{}, err
}
data := DescriptionToFont(ft.Describe())
data := DescriptionToFont(metadata.Metadata(ld))
return ft, data, nil
}
// Face returns a thread-unsafe wrapper for this Face suitable for use by a single shaper.
// Face many be invoked any number of times and is safe so long as each return value is
// only used by one goroutine.
func (f Face) Face() *fontapi.Face {
func (f Face) Face() font.Face {
return &fontapi.Face{Font: f.face}
}
@@ -117,74 +119,74 @@ func (f Face) Font() giofont.Font {
return f.font
}
func gioStyle(s fontapi.Style) giofont.Style {
func gioStyle(s metadata.Style) giofont.Style {
switch s {
case fontapi.StyleItalic:
case metadata.StyleItalic:
return giofont.Italic
case fontapi.StyleNormal:
case metadata.StyleNormal:
fallthrough
default:
return giofont.Regular
}
}
func mdStyle(g giofont.Style) fontapi.Style {
func mdStyle(g giofont.Style) metadata.Style {
switch g {
case giofont.Italic:
return fontapi.StyleItalic
return metadata.StyleItalic
case giofont.Regular:
fallthrough
default:
return fontapi.StyleNormal
return metadata.StyleNormal
}
}
func gioWeight(w fontapi.Weight) giofont.Weight {
func gioWeight(w metadata.Weight) giofont.Weight {
switch w {
case fontapi.WeightThin:
case metadata.WeightThin:
return giofont.Thin
case fontapi.WeightExtraLight:
case metadata.WeightExtraLight:
return giofont.ExtraLight
case fontapi.WeightLight:
case metadata.WeightLight:
return giofont.Light
case fontapi.WeightNormal:
case metadata.WeightNormal:
return giofont.Normal
case fontapi.WeightMedium:
case metadata.WeightMedium:
return giofont.Medium
case fontapi.WeightSemibold:
case metadata.WeightSemibold:
return giofont.SemiBold
case fontapi.WeightBold:
case metadata.WeightBold:
return giofont.Bold
case fontapi.WeightExtraBold:
case metadata.WeightExtraBold:
return giofont.ExtraBold
case fontapi.WeightBlack:
case metadata.WeightBlack:
return giofont.Black
default:
return giofont.Normal
}
}
func mdWeight(g giofont.Weight) fontapi.Weight {
func mdWeight(g giofont.Weight) metadata.Weight {
switch g {
case giofont.Thin:
return fontapi.WeightThin
return metadata.WeightThin
case giofont.ExtraLight:
return fontapi.WeightExtraLight
return metadata.WeightExtraLight
case giofont.Light:
return fontapi.WeightLight
return metadata.WeightLight
case giofont.Normal:
return fontapi.WeightNormal
return metadata.WeightNormal
case giofont.Medium:
return fontapi.WeightMedium
return metadata.WeightMedium
case giofont.SemiBold:
return fontapi.WeightSemibold
return metadata.WeightSemibold
case giofont.Bold:
return fontapi.WeightBold
return metadata.WeightBold
case giofont.ExtraBold:
return fontapi.WeightExtraBold
return metadata.WeightExtraBold
case giofont.Black:
return fontapi.WeightBlack
return metadata.WeightBlack
default:
return fontapi.WeightNormal
return metadata.WeightNormal
}
}
+4 -5
View File
@@ -271,13 +271,12 @@ 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, scrollx, scrolly pointer.ScrollRange) int {
func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, bounds image.Rectangle) int {
total := 0
f := pointer.Filter{
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollX: scrollx,
ScrollY: scrolly,
Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollBounds: bounds,
}
for {
evt, ok := q.Event(f)
+9 -7
View File
@@ -1,14 +1,16 @@
module gioui.org
go 1.21
go 1.19
require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.8
github.com/go-text/typesetting v0.2.1
golang.org/x/exp v0.0.0-20240707233637-46b078467d37
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37
golang.org/x/image v0.18.0
golang.org/x/sys v0.22.0
golang.org/x/text v0.16.0
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
golang.org/x/image v0.5.0
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64
)
require golang.org/x/text v0.7.0
+38 -14
View File
@@ -1,19 +1,43 @@
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w=
golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37 h1:SOSg7+sueresE4IbmmGM60GmlIys+zNX63d6/J4CMtU=
golang.org/x/exp/shiny v0.0.0-20240707233637-46b078467d37/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
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=
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95 h1:sBdrWpxhGDdTAYNqbgBLAR+ULAPPhfgncLr1X0lyWtg=
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0=
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8=
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/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-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.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.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+2193
View File
File diff suppressed because it is too large Load Diff
+129
View File
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: Unlicense OR MIT
package gpu
import (
"unsafe"
"gioui.org/cpu"
)
// This file contains code specific to running compute shaders on the CPU.
// dispatcher dispatches CPU compute programs across multiple goroutines.
type dispatcher struct {
// done is notified when a worker completes its work slice.
done chan struct{}
// work receives work slice indices. It is closed when the dispatcher is released.
work chan work
// dispatch receives compute jobs, which is then split among workers.
dispatch chan dispatch
// sync receives notification when a Sync completes.
sync chan struct{}
}
type work struct {
ctx *cpu.DispatchContext
index int
}
type dispatch struct {
_type jobType
program *cpu.ProgramInfo
descSet unsafe.Pointer
x, y, z int
}
type jobType uint8
const (
jobDispatch jobType = iota
jobBarrier
jobSync
)
func newDispatcher(workers int) *dispatcher {
d := &dispatcher{
work: make(chan work, workers),
done: make(chan struct{}, workers),
// Leave some room to avoid blocking calls to Dispatch.
dispatch: make(chan dispatch, 20),
sync: make(chan struct{}),
}
for i := 0; i < workers; i++ {
go d.worker()
}
go d.dispatcher()
return d
}
func (d *dispatcher) dispatcher() {
defer close(d.work)
var free []*cpu.DispatchContext
defer func() {
for _, ctx := range free {
ctx.Free()
}
}()
var used []*cpu.DispatchContext
for job := range d.dispatch {
switch job._type {
case jobDispatch:
if len(free) == 0 {
free = append(free, cpu.NewDispatchContext())
}
ctx := free[len(free)-1]
free = free[:len(free)-1]
used = append(used, ctx)
ctx.Prepare(cap(d.work), job.program, job.descSet, job.x, job.y, job.z)
for i := 0; i < cap(d.work); i++ {
d.work <- work{
ctx: ctx,
index: i,
}
}
case jobBarrier:
// Wait for all outstanding dispatches to complete.
for i := 0; i < len(used)*cap(d.work); i++ {
<-d.done
}
free = append(free, used...)
used = used[:0]
case jobSync:
d.sync <- struct{}{}
}
}
}
func (d *dispatcher) worker() {
thread := cpu.NewThreadContext()
defer thread.Free()
for w := range d.work {
w.ctx.Dispatch(w.index, thread)
d.done <- struct{}{}
}
}
func (d *dispatcher) Barrier() {
d.dispatch <- dispatch{_type: jobBarrier}
}
func (d *dispatcher) Sync() {
d.dispatch <- dispatch{_type: jobSync}
<-d.sync
}
func (d *dispatcher) Dispatch(program *cpu.ProgramInfo, descSet unsafe.Pointer, x, y, z int) {
d.dispatch <- dispatch{
_type: jobDispatch,
program: program,
descSet: descSet,
x: x,
y: y,
z: z,
}
}
func (d *dispatcher) Stop() {
close(d.dispatch)
}
+94 -115
View File
@@ -9,11 +9,11 @@ package gpu
import (
"encoding/binary"
"errors"
"fmt"
"image"
"image/color"
"math"
"os"
"reflect"
"time"
"unsafe"
@@ -261,7 +261,7 @@ type texture struct {
type blitter struct {
ctx driver.Device
viewport image.Point
pipelines [2][3]*pipeline
pipelines [3]*pipeline
colUniforms *blitColUniforms
texUniforms *blitTexUniforms
linearGradientUniforms *blitLinearGradientUniforms
@@ -343,12 +343,13 @@ func New(api API) (GPU, error) {
func NewWithDevice(d driver.Device) (GPU, error) {
d.BeginFrame(nil, false, image.Point{})
defer d.EndFrame()
forceCompute := os.Getenv("GIORENDERER") == "forcecompute"
feats := d.Caps().Features
switch {
case feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB):
case !forceCompute && feats.Has(driver.FeatureFloatRenderTargets) && feats.Has(driver.FeatureSRGB):
return newGPU(d)
}
return nil, errors.New("no available GPU driver")
return newCompute(d)
}
func newGPU(ctx driver.Device) (*gpu, error) {
@@ -559,24 +560,12 @@ func newBlitter(ctx driver.Device) *blitter {
func (b *blitter) release() {
b.quadVerts.Release()
for _, p := range b.pipelines {
for _, p := range p {
p.Release()
}
p.Release()
}
}
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()
}
}
}
}
}()
func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.Sources, uniforms [3]interface{}) ([3]*pipeline, error) {
var pipelines [3]*pipeline
blend := driver.BlendDesc{
Enable: true,
SrcFactor: driver.BlendFactorOne,
@@ -594,76 +583,86 @@ func createColorPrograms(b driver.Device, vsSrc shader.Sources, fsSrc [3]shader.
return pipelines, err
}
defer vsh.Release()
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}
{
fsh, err := b.NewFragmentShader(fsSrc[materialTexture])
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}
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[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}
var vertBuffer *uniformBuffer
if u := uniforms[materialTexture]; u != nil {
vertBuffer = newUniformBuffer(b, u)
}
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
}
@@ -866,7 +865,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.Dx(), v.Dy())
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y)
f := r.layerFBOs.fbos[fbo]
r.drawOps(true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
sr := f32.FRect(v)
@@ -931,7 +930,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) {
func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey opKey, bounds f32.Rectangle, off f32.Point, push bool) {
npath := d.newPathOp()
*npath = pathOp{
parent: state.cpath,
@@ -1056,7 +1055,7 @@ loop:
quads.aux, bounds, _ = d.boundsForTransformedRect(bounds, trans)
quads.key = opKey{Key: encOp.Key}
}
d.addClipPath(&state, quads.aux, quads.key, bounds, off)
d.addClipPath(&state, quads.aux, quads.key, bounds, off, true)
quads = quadsOp{}
case ops.TypePopClip:
state.cpath = state.cpath.parent
@@ -1101,7 +1100,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)
d.addClipPath(&state, clipData, k, bnd, off, false)
}
bounds := cl.Round()
@@ -1231,7 +1230,7 @@ func (r *renderer) prepareDrawOps(ops []imageOp) {
}
}
func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageOp) {
func (r *renderer) drawOps(isFBO bool, opOff image.Point, viewport image.Point, ops []imageOp) {
var coverTex driver.Texture
for i := 0; i < len(ops); i++ {
img := ops[i]
@@ -1245,13 +1244,9 @@ func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageO
scale, off := clipSpaceTransform(drc, viewport)
var fbo FBO
fboIdx := 0
if isFBO {
fboIdx = 1
}
switch img.clipType {
case clipTypeNone:
p := r.blitter.pipelines[fboIdx][m.material]
p := r.blitter.pipelines[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)
@@ -1270,7 +1265,7 @@ func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageO
Max: img.place.Pos.Add(drc.Size()),
}
coverScale, coverOff := texSpaceTransform(f32.FRect(uv), fbo.size)
p := r.pather.coverer.pipelines[fboIdx][m.material]
p := r.pather.coverer.pipelines[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)
@@ -1278,11 +1273,7 @@ func (r *renderer) drawOps(isFBO bool, opOff, viewport image.Point, ops []imageO
}
func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) {
fboIdx := 0
if fbo {
fboIdx = 1
}
p := b.pipelines[fboIdx][mat]
p := b.pipelines[mat]
b.ctx.BindPipeline(p.pipeline)
var uniforms *blitUniforms
switch mat {
@@ -1483,7 +1474,7 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str
// as needed and feeds them to the supplied splitter.
func decodeToOutlineQuads(qs *quadSplitter, tr f32.Affine2D, pathData []byte) {
for len(pathData) >= scene.CommandSize+4 {
qs.contour = binary.LittleEndian.Uint32(pathData)
qs.contour = bo.Uint32(pathData)
cmd := ops.DecodeCommand(pathData[4:])
switch cmd.Op() {
case scene.OpLine:
@@ -1578,15 +1569,3 @@ func isPureOffset(t f32.Affine2D) bool {
a, b, _, d, e, _ := t.Elems()
return a == 1 && b == 0 && d == 0 && e == 1
}
func newShaders(ctx driver.Device, vsrc, fsrc shader.Sources) (vert driver.VertexShader, frag driver.FragmentShader, err error) {
vert, err = ctx.NewVertexShader(vsrc)
if err != nil {
return
}
frag, err = ctx.NewFragmentShader(fsrc)
if err != nil {
vert.Release()
}
return
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 334 B

+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 exercise the glClear optimization.
// Fill screen to exercize the glClear optimization.
paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op())
opc2 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op())
opc2.Pop()
opc1.Pop()
opc3 := paint.PushOpacity(ops, .6)
paint.FillShape(ops, color.NRGBA{B: 255, A: 255}, clip.Ellipse(image.Rectangle{Min: image.Pt(20+20, 10), Max: image.Pt(50+64, 128)}).Op(ops))
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(50+20, 10), Max: image.Pt(50+64, 128)}.Op())
opc3.Pop()
}, func(r result) {
})
+2 -2
View File
@@ -862,8 +862,8 @@ func (b *Backend) BindUniforms(buffer driver.Buffer) {
buf := buffer.(*Buffer)
cmdBuf := b.currentCmdBuf()
for _, s := range b.pipe.pushRanges {
off := vk.PushConstantRangeOffset(s)
vk.CmdPushConstants(cmdBuf, b.pipe.desc.layout, vk.PushConstantRangeStageFlags(s), off, buf.store[off:off+vk.PushConstantRangeSize(s)])
off := s.Offset()
vk.CmdPushConstants(cmdBuf, b.pipe.desc.layout, s.StageFlags(), off, buf.store[off:off+s.Size()])
}
}
+3 -9
View File
@@ -30,7 +30,7 @@ type pather struct {
type coverer struct {
ctx driver.Device
pipelines [2][3]*pipeline
pipelines [3]*pipeline
texUniforms *coverTexUniforms
colUniforms *coverColUniforms
linearGradientUniforms *coverLinearGradientUniforms
@@ -309,9 +309,7 @@ func (p *pather) release() {
func (c *coverer) release() {
for _, p := range c.pipelines {
for _, p := range p {
p.Release()
}
p.Release()
}
}
@@ -407,11 +405,7 @@ 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}
fboIdx := 0
if isFBO {
fboIdx = 1
}
c.pipelines[fboIdx][mat].UploadUniforms(c.ctx)
c.pipelines[mat].UploadUniforms(c.ctx)
c.ctx.DrawArrays(0, 4)
}
+7 -4
View File
@@ -15,9 +15,10 @@ import (
)
type Context struct {
disp _EGLDisplay
eglCtx *eglContext
eglSurf _EGLSurface
disp _EGLDisplay
eglCtx *eglContext
eglSurf _EGLSurface
width, height int
}
type eglContext struct {
@@ -120,9 +121,11 @@ func (c *Context) VisualID() int {
return c.eglCtx.visualID
}
func (c *Context) CreateSurface(win NativeWindowType) error {
func (c *Context) CreateSurface(win NativeWindowType, width, height int) error {
eglSurf, err := createSurface(c.disp, c.eglCtx, win)
c.eglSurf = eglSurf
c.width = width
c.height = height
return err
}
+26 -48
View File
@@ -9,6 +9,8 @@ import (
"unsafe"
syscall "golang.org/x/sys/windows"
"gioui.org/internal/gl"
)
type (
@@ -22,23 +24,23 @@ type (
)
var (
libEGL = syscall.DLL{}
_eglChooseConfig *syscall.Proc
_eglCreateContext *syscall.Proc
_eglCreateWindowSurface *syscall.Proc
_eglDestroyContext *syscall.Proc
_eglDestroySurface *syscall.Proc
_eglGetConfigAttrib *syscall.Proc
_eglGetDisplay *syscall.Proc
_eglGetError *syscall.Proc
_eglInitialize *syscall.Proc
_eglMakeCurrent *syscall.Proc
_eglReleaseThread *syscall.Proc
_eglSwapInterval *syscall.Proc
_eglSwapBuffers *syscall.Proc
_eglTerminate *syscall.Proc
_eglQueryString *syscall.Proc
_eglWaitClient *syscall.Proc
libEGL = syscall.NewLazyDLL("libEGL.dll")
_eglChooseConfig = libEGL.NewProc("eglChooseConfig")
_eglCreateContext = libEGL.NewProc("eglCreateContext")
_eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface")
_eglDestroyContext = libEGL.NewProc("eglDestroyContext")
_eglDestroySurface = libEGL.NewProc("eglDestroySurface")
_eglGetConfigAttrib = libEGL.NewProc("eglGetConfigAttrib")
_eglGetDisplay = libEGL.NewProc("eglGetDisplay")
_eglGetError = libEGL.NewProc("eglGetError")
_eglInitialize = libEGL.NewProc("eglInitialize")
_eglMakeCurrent = libEGL.NewProc("eglMakeCurrent")
_eglReleaseThread = libEGL.NewProc("eglReleaseThread")
_eglSwapInterval = libEGL.NewProc("eglSwapInterval")
_eglSwapBuffers = libEGL.NewProc("eglSwapBuffers")
_eglTerminate = libEGL.NewProc("eglTerminate")
_eglQueryString = libEGL.NewProc("eglQueryString")
_eglWaitClient = libEGL.NewProc("eglWaitClient")
)
var loadOnce sync.Once
@@ -52,45 +54,21 @@ func loadEGL() error {
}
func loadDLLs() error {
if err := loadDLL(&libEGL, "libEGL.dll"); err != nil {
if err := loadDLL(libEGL, "libEGL.dll"); err != nil {
return err
}
procs := map[string]**syscall.Proc{
"eglChooseConfig": &_eglChooseConfig,
"eglCreateContext": &_eglCreateContext,
"eglCreateWindowSurface": &_eglCreateWindowSurface,
"eglDestroyContext": &_eglDestroyContext,
"eglDestroySurface": &_eglDestroySurface,
"eglGetConfigAttrib": &_eglGetConfigAttrib,
"eglGetDisplay": &_eglGetDisplay,
"eglGetError": &_eglGetError,
"eglInitialize": &_eglInitialize,
"eglMakeCurrent": &_eglMakeCurrent,
"eglReleaseThread": &_eglReleaseThread,
"eglSwapInterval": &_eglSwapInterval,
"eglSwapBuffers": &_eglSwapBuffers,
"eglTerminate": &_eglTerminate,
"eglQueryString": &_eglQueryString,
"eglWaitClient": &_eglWaitClient,
if err := loadDLL(gl.LibGLESv2, "libGLESv2.dll"); err != nil {
return err
}
for name, proc := range procs {
p, err := libEGL.FindProc(name)
if err != nil {
return fmt.Errorf("failed to locate %s in %s: %w", name, libEGL.Name, err)
}
*proc = p
}
return nil
// d3dcompiler_47.dll is needed internally for shader compilation to function.
return loadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"), "d3dcompiler_47.dll")
}
func loadDLL(dll *syscall.DLL, name string) error {
handle, err := syscall.LoadLibraryEx(name, 0, syscall.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)
func loadDLL(dll *syscall.LazyDLL, name string) error {
err := dll.Load()
if err != nil {
return fmt.Errorf("egl: failed to load %s: %v", name, err)
}
dll.Handle = handle
dll.Name = name
return nil
}
+89 -210
View File
@@ -3,217 +3,103 @@
package gl
import (
"fmt"
"math"
"runtime"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
func loadGLESv2Procs() error {
dllName := "libGLESv2.dll"
handle, err := windows.LoadLibraryEx(dllName, 0, windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)
if err != nil {
return fmt.Errorf("gl: failed to load %s: %v", dllName, err)
}
gles := windows.DLL{Handle: handle, Name: dllName}
// d3dcompiler_47.dll is needed internally for shader compilation to function.
dllName = "d3dcompiler_47.dll"
_, err = windows.LoadLibraryEx(dllName, 0, windows.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)
if err != nil {
return fmt.Errorf("gl: failed to load %s: %v", dllName, err)
}
procs := map[string]**windows.Proc{
"glActiveTexture": &_glActiveTexture,
"glAttachShader": &_glAttachShader,
"glBeginQuery": &_glBeginQuery,
"glBindAttribLocation": &_glBindAttribLocation,
"glBindBuffer": &_glBindBuffer,
"glBindBufferBase": &_glBindBufferBase,
"glBindFramebuffer": &_glBindFramebuffer,
"glBindRenderbuffer": &_glBindRenderbuffer,
"glBindTexture": &_glBindTexture,
"glBindVertexArray": &_glBindVertexArray,
"glBlendEquation": &_glBlendEquation,
"glBlendFuncSeparate": &_glBlendFuncSeparate,
"glBufferData": &_glBufferData,
"glBufferSubData": &_glBufferSubData,
"glCheckFramebufferStatus": &_glCheckFramebufferStatus,
"glClear": &_glClear,
"glClearColor": &_glClearColor,
"glClearDepthf": &_glClearDepthf,
"glDeleteQueries": &_glDeleteQueries,
"glDeleteVertexArrays": &_glDeleteVertexArrays,
"glCompileShader": &_glCompileShader,
"glCopyTexSubImage2D": &_glCopyTexSubImage2D,
"glGenerateMipmap": &_glGenerateMipmap,
"glGenBuffers": &_glGenBuffers,
"glGenFramebuffers": &_glGenFramebuffers,
"glGenVertexArrays": &_glGenVertexArrays,
"glGetUniformBlockIndex": &_glGetUniformBlockIndex,
"glCreateProgram": &_glCreateProgram,
"glGenRenderbuffers": &_glGenRenderbuffers,
"glCreateShader": &_glCreateShader,
"glGenTextures": &_glGenTextures,
"glDeleteBuffers": &_glDeleteBuffers,
"glDeleteFramebuffers": &_glDeleteFramebuffers,
"glDeleteProgram": &_glDeleteProgram,
"glDeleteShader": &_glDeleteShader,
"glDeleteRenderbuffers": &_glDeleteRenderbuffers,
"glDeleteTextures": &_glDeleteTextures,
"glDepthFunc": &_glDepthFunc,
"glDepthMask": &_glDepthMask,
"glDisableVertexAttribArray": &_glDisableVertexAttribArray,
"glDisable": &_glDisable,
"glDrawArrays": &_glDrawArrays,
"glDrawElements": &_glDrawElements,
"glEnable": &_glEnable,
"glEnableVertexAttribArray": &_glEnableVertexAttribArray,
"glEndQuery": &_glEndQuery,
"glFinish": &_glFinish,
"glFlush": &_glFlush,
"glFramebufferRenderbuffer": &_glFramebufferRenderbuffer,
"glFramebufferTexture2D": &_glFramebufferTexture2D,
"glGenQueries": &_glGenQueries,
"glGetError": &_glGetError,
"glGetRenderbufferParameteriv": &_glGetRenderbufferParameteriv,
"glGetFloatv": &_glGetFloatv,
"glGetFramebufferAttachmentParameteriv": &_glGetFramebufferAttachmentParameteriv,
"glGetIntegerv": &_glGetIntegerv,
"glGetIntegeri_v": &_glGetIntegeri_v,
"glGetProgramiv": &_glGetProgramiv,
"glGetProgramInfoLog": &_glGetProgramInfoLog,
"glGetQueryObjectuiv": &_glGetQueryObjectuiv,
"glGetShaderiv": &_glGetShaderiv,
"glGetShaderInfoLog": &_glGetShaderInfoLog,
"glGetString": &_glGetString,
"glGetUniformLocation": &_glGetUniformLocation,
"glGetVertexAttribiv": &_glGetVertexAttribiv,
"glGetVertexAttribPointerv": &_glGetVertexAttribPointerv,
"glInvalidateFramebuffer": &_glInvalidateFramebuffer,
"glIsEnabled": &_glIsEnabled,
"glLinkProgram": &_glLinkProgram,
"glPixelStorei": &_glPixelStorei,
"glReadPixels": &_glReadPixels,
"glRenderbufferStorage": &_glRenderbufferStorage,
"glScissor": &_glScissor,
"glShaderSource": &_glShaderSource,
"glTexImage2D": &_glTexImage2D,
"glTexStorage2D": &_glTexStorage2D,
"glTexSubImage2D": &_glTexSubImage2D,
"glTexParameteri": &_glTexParameteri,
"glUniformBlockBinding": &_glUniformBlockBinding,
"glUniform1f": &_glUniform1f,
"glUniform1i": &_glUniform1i,
"glUniform2f": &_glUniform2f,
"glUniform3f": &_glUniform3f,
"glUniform4f": &_glUniform4f,
"glUseProgram": &_glUseProgram,
"glVertexAttribPointer": &_glVertexAttribPointer,
"glViewport": &_glViewport,
}
for name, proc := range procs {
p, err := gles.FindProc(name)
if err != nil {
return fmt.Errorf("failed to locate %s in %s: %w", name, gles.Name, err)
}
*proc = p
}
return nil
}
var (
glInitOnce sync.Once
_glActiveTexture *windows.Proc
_glAttachShader *windows.Proc
_glBeginQuery *windows.Proc
_glBindAttribLocation *windows.Proc
_glBindBuffer *windows.Proc
_glBindBufferBase *windows.Proc
_glBindFramebuffer *windows.Proc
_glBindRenderbuffer *windows.Proc
_glBindTexture *windows.Proc
_glBindVertexArray *windows.Proc
_glBlendEquation *windows.Proc
_glBlendFuncSeparate *windows.Proc
_glBufferData *windows.Proc
_glBufferSubData *windows.Proc
_glCheckFramebufferStatus *windows.Proc
_glClear *windows.Proc
_glClearColor *windows.Proc
_glClearDepthf *windows.Proc
_glDeleteQueries *windows.Proc
_glDeleteVertexArrays *windows.Proc
_glCompileShader *windows.Proc
_glCopyTexSubImage2D *windows.Proc
_glGenerateMipmap *windows.Proc
_glGenBuffers *windows.Proc
_glGenFramebuffers *windows.Proc
_glGenVertexArrays *windows.Proc
_glGetUniformBlockIndex *windows.Proc
_glCreateProgram *windows.Proc
_glGenRenderbuffers *windows.Proc
_glCreateShader *windows.Proc
_glGenTextures *windows.Proc
_glDeleteBuffers *windows.Proc
_glDeleteFramebuffers *windows.Proc
_glDeleteProgram *windows.Proc
_glDeleteShader *windows.Proc
_glDeleteRenderbuffers *windows.Proc
_glDeleteTextures *windows.Proc
_glDepthFunc *windows.Proc
_glDepthMask *windows.Proc
_glDisableVertexAttribArray *windows.Proc
_glDisable *windows.Proc
_glDrawArrays *windows.Proc
_glDrawElements *windows.Proc
_glEnable *windows.Proc
_glEnableVertexAttribArray *windows.Proc
_glEndQuery *windows.Proc
_glFinish *windows.Proc
_glFlush *windows.Proc
_glFramebufferRenderbuffer *windows.Proc
_glFramebufferTexture2D *windows.Proc
_glGenQueries *windows.Proc
_glGetError *windows.Proc
_glGetRenderbufferParameteriv *windows.Proc
_glGetFloatv *windows.Proc
_glGetFramebufferAttachmentParameteriv *windows.Proc
_glGetIntegerv *windows.Proc
_glGetIntegeri_v *windows.Proc
_glGetProgramiv *windows.Proc
_glGetProgramInfoLog *windows.Proc
_glGetQueryObjectuiv *windows.Proc
_glGetShaderiv *windows.Proc
_glGetShaderInfoLog *windows.Proc
_glGetString *windows.Proc
_glGetUniformLocation *windows.Proc
_glGetVertexAttribiv *windows.Proc
_glGetVertexAttribPointerv *windows.Proc
_glInvalidateFramebuffer *windows.Proc
_glIsEnabled *windows.Proc
_glLinkProgram *windows.Proc
_glPixelStorei *windows.Proc
_glReadPixels *windows.Proc
_glRenderbufferStorage *windows.Proc
_glScissor *windows.Proc
_glShaderSource *windows.Proc
_glTexImage2D *windows.Proc
_glTexStorage2D *windows.Proc
_glTexSubImage2D *windows.Proc
_glTexParameteri *windows.Proc
_glUniformBlockBinding *windows.Proc
_glUniform1f *windows.Proc
_glUniform1i *windows.Proc
_glUniform2f *windows.Proc
_glUniform3f *windows.Proc
_glUniform4f *windows.Proc
_glUseProgram *windows.Proc
_glVertexAttribPointer *windows.Proc
_glViewport *windows.Proc
LibGLESv2 = windows.NewLazyDLL("libGLESv2.dll")
_glActiveTexture = LibGLESv2.NewProc("glActiveTexture")
_glAttachShader = LibGLESv2.NewProc("glAttachShader")
_glBeginQuery = LibGLESv2.NewProc("glBeginQuery")
_glBindAttribLocation = LibGLESv2.NewProc("glBindAttribLocation")
_glBindBuffer = LibGLESv2.NewProc("glBindBuffer")
_glBindBufferBase = LibGLESv2.NewProc("glBindBufferBase")
_glBindFramebuffer = LibGLESv2.NewProc("glBindFramebuffer")
_glBindRenderbuffer = LibGLESv2.NewProc("glBindRenderbuffer")
_glBindTexture = LibGLESv2.NewProc("glBindTexture")
_glBindVertexArray = LibGLESv2.NewProc("glBindVertexArray")
_glBlendEquation = LibGLESv2.NewProc("glBlendEquation")
_glBlendFuncSeparate = LibGLESv2.NewProc("glBlendFuncSeparate")
_glBufferData = LibGLESv2.NewProc("glBufferData")
_glBufferSubData = LibGLESv2.NewProc("glBufferSubData")
_glCheckFramebufferStatus = LibGLESv2.NewProc("glCheckFramebufferStatus")
_glClear = LibGLESv2.NewProc("glClear")
_glClearColor = LibGLESv2.NewProc("glClearColor")
_glClearDepthf = LibGLESv2.NewProc("glClearDepthf")
_glDeleteQueries = LibGLESv2.NewProc("glDeleteQueries")
_glDeleteVertexArrays = LibGLESv2.NewProc("glDeleteVertexArrays")
_glCompileShader = LibGLESv2.NewProc("glCompileShader")
_glCopyTexSubImage2D = LibGLESv2.NewProc("glCopyTexSubImage2D")
_glGenerateMipmap = LibGLESv2.NewProc("glGenerateMipmap")
_glGenBuffers = LibGLESv2.NewProc("glGenBuffers")
_glGenFramebuffers = LibGLESv2.NewProc("glGenFramebuffers")
_glGenVertexArrays = LibGLESv2.NewProc("glGenVertexArrays")
_glGetUniformBlockIndex = LibGLESv2.NewProc("glGetUniformBlockIndex")
_glCreateProgram = LibGLESv2.NewProc("glCreateProgram")
_glGenRenderbuffers = LibGLESv2.NewProc("glGenRenderbuffers")
_glCreateShader = LibGLESv2.NewProc("glCreateShader")
_glGenTextures = LibGLESv2.NewProc("glGenTextures")
_glDeleteBuffers = LibGLESv2.NewProc("glDeleteBuffers")
_glDeleteFramebuffers = LibGLESv2.NewProc("glDeleteFramebuffers")
_glDeleteProgram = LibGLESv2.NewProc("glDeleteProgram")
_glDeleteShader = LibGLESv2.NewProc("glDeleteShader")
_glDeleteRenderbuffers = LibGLESv2.NewProc("glDeleteRenderbuffers")
_glDeleteTextures = LibGLESv2.NewProc("glDeleteTextures")
_glDepthFunc = LibGLESv2.NewProc("glDepthFunc")
_glDepthMask = LibGLESv2.NewProc("glDepthMask")
_glDisableVertexAttribArray = LibGLESv2.NewProc("glDisableVertexAttribArray")
_glDisable = LibGLESv2.NewProc("glDisable")
_glDrawArrays = LibGLESv2.NewProc("glDrawArrays")
_glDrawElements = LibGLESv2.NewProc("glDrawElements")
_glEnable = LibGLESv2.NewProc("glEnable")
_glEnableVertexAttribArray = LibGLESv2.NewProc("glEnableVertexAttribArray")
_glEndQuery = LibGLESv2.NewProc("glEndQuery")
_glFinish = LibGLESv2.NewProc("glFinish")
_glFlush = LibGLESv2.NewProc("glFlush")
_glFramebufferRenderbuffer = LibGLESv2.NewProc("glFramebufferRenderbuffer")
_glFramebufferTexture2D = LibGLESv2.NewProc("glFramebufferTexture2D")
_glGenQueries = LibGLESv2.NewProc("glGenQueries")
_glGetError = LibGLESv2.NewProc("glGetError")
_glGetRenderbufferParameteriv = LibGLESv2.NewProc("glGetRenderbufferParameteriv")
_glGetFloatv = LibGLESv2.NewProc("glGetFloatv")
_glGetFramebufferAttachmentParameteriv = LibGLESv2.NewProc("glGetFramebufferAttachmentParameteriv")
_glGetIntegerv = LibGLESv2.NewProc("glGetIntegerv")
_glGetIntegeri_v = LibGLESv2.NewProc("glGetIntegeri_v")
_glGetProgramiv = LibGLESv2.NewProc("glGetProgramiv")
_glGetProgramInfoLog = LibGLESv2.NewProc("glGetProgramInfoLog")
_glGetQueryObjectuiv = LibGLESv2.NewProc("glGetQueryObjectuiv")
_glGetShaderiv = LibGLESv2.NewProc("glGetShaderiv")
_glGetShaderInfoLog = LibGLESv2.NewProc("glGetShaderInfoLog")
_glGetString = LibGLESv2.NewProc("glGetString")
_glGetUniformLocation = LibGLESv2.NewProc("glGetUniformLocation")
_glGetVertexAttribiv = LibGLESv2.NewProc("glGetVertexAttribiv")
_glGetVertexAttribPointerv = LibGLESv2.NewProc("glGetVertexAttribPointerv")
_glInvalidateFramebuffer = LibGLESv2.NewProc("glInvalidateFramebuffer")
_glIsEnabled = LibGLESv2.NewProc("glIsEnabled")
_glLinkProgram = LibGLESv2.NewProc("glLinkProgram")
_glPixelStorei = LibGLESv2.NewProc("glPixelStorei")
_glReadPixels = LibGLESv2.NewProc("glReadPixels")
_glRenderbufferStorage = LibGLESv2.NewProc("glRenderbufferStorage")
_glScissor = LibGLESv2.NewProc("glScissor")
_glShaderSource = LibGLESv2.NewProc("glShaderSource")
_glTexImage2D = LibGLESv2.NewProc("glTexImage2D")
_glTexStorage2D = LibGLESv2.NewProc("glTexStorage2D")
_glTexSubImage2D = LibGLESv2.NewProc("glTexSubImage2D")
_glTexParameteri = LibGLESv2.NewProc("glTexParameteri")
_glUniformBlockBinding = LibGLESv2.NewProc("glUniformBlockBinding")
_glUniform1f = LibGLESv2.NewProc("glUniform1f")
_glUniform1i = LibGLESv2.NewProc("glUniform1i")
_glUniform2f = LibGLESv2.NewProc("glUniform2f")
_glUniform3f = LibGLESv2.NewProc("glUniform3f")
_glUniform4f = LibGLESv2.NewProc("glUniform4f")
_glUseProgram = LibGLESv2.NewProc("glUseProgram")
_glVertexAttribPointer = LibGLESv2.NewProc("glVertexAttribPointer")
_glViewport = LibGLESv2.NewProc("glViewport")
)
type Functions struct {
@@ -229,11 +115,7 @@ func NewFunctions(ctx Context, forceES bool) (*Functions, error) {
if ctx != nil {
panic("non-nil context")
}
var err error
glInitOnce.Do(func() {
err = loadGLESv2Procs()
})
return new(Functions), err
return new(Functions), nil
}
func (c *Functions) ActiveTexture(t Enum) {
@@ -479,9 +361,6 @@ 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,9 +327,6 @@ 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 {
+6 -6
View File
@@ -1893,27 +1893,27 @@ func BuildWriteDescriptorSetBuffer(set DescriptorSet, binding int, typ Descripto
}
}
func PushConstantRangeStageFlags(r PushConstantRange) ShaderStageFlags {
func (r PushConstantRange) StageFlags() ShaderStageFlags {
return r.stageFlags
}
func PushConstantRangeOffset(r PushConstantRange) int {
func (r PushConstantRange) Offset() int {
return int(r.offset)
}
func PushConstantRangeSize(r PushConstantRange) int {
func (r PushConstantRange) Size() int {
return int(r.size)
}
func QueueFamilyPropertiesFlags(p QueueFamilyProperties) QueueFlags {
func (p QueueFamilyProperties) Flags() QueueFlags {
return p.queueFlags
}
func SurfaceCapabilitiesMinExtent(c SurfaceCapabilities) image.Point {
func (c SurfaceCapabilities) MinExtent() image.Point {
return image.Pt(int(c.minImageExtent.width), int(c.minImageExtent.height))
}
func SurfaceCapabilitiesMaxExtent(c SurfaceCapabilities) image.Point {
func (c SurfaceCapabilities) MaxExtent() image.Point {
return image.Pt(int(c.maxImageExtent.width), int(c.maxImageExtent.height))
}
+3 -4
View File
@@ -251,10 +251,9 @@ func TestFocusScroll(t *testing.T) {
filters := []event.Filter{
key.FocusFilter{Target: h},
pointer.Filter{
Target: h,
Kinds: pointer.Scroll,
ScrollX: pointer.ScrollRange{Min: -100, Max: +100},
ScrollY: pointer.ScrollRange{Min: -100, Max: +100},
Target: h,
Kinds: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100),
},
}
events(r, -1, filters...)
+5 -7
View File
@@ -72,7 +72,7 @@ type pointerHandler struct {
type pointerFilter struct {
kinds pointer.Kind
// min and max horizontal/vertical scroll
scrollX, scrollY pointer.ScrollRange
scrollRange image.Rectangle
sourceMimes []string
targetMimes []string
@@ -297,8 +297,7 @@ func (p *pointerFilter) Add(f event.Filter) {
p.targetMimes = append(p.targetMimes, f.Type)
case pointer.Filter:
p.kinds = p.kinds | f.Kinds
p.scrollX = p.scrollX.Union(f.ScrollX)
p.scrollY = p.scrollY.Union(f.ScrollY)
p.scrollRange = p.scrollRange.Union(f.ScrollBounds)
}
}
@@ -326,8 +325,7 @@ func (p *pointerFilter) Matches(e event.Event) bool {
func (p *pointerFilter) Merge(p2 pointerFilter) {
p.kinds = p.kinds | p2.kinds
p.scrollX = p.scrollX.Union(p2.scrollX)
p.scrollY = p.scrollY.Union(p2.scrollY)
p.scrollRange = p.scrollRange.Union(p2.scrollRange)
p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...)
p.targetMimes = append(p.targetMimes, p2.targetMimes...)
}
@@ -335,8 +333,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.scrollX.Min, p.scrollX.Max)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max)
left.X, scrolled.X = clampSplit(scroll.X, p.scrollRange.Min.X, p.scrollRange.Max.X)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollRange.Min.Y, p.scrollRange.Max.Y)
return
}
+9 -37
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,
ScrollX: pointer.ScrollRange{Max: 100},
Target: t,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 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,
ScrollX: pointer.ScrollRange{Max: 20},
Target: t,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
}
}
events(&r, -1, f2(handler2))
@@ -324,10 +324,9 @@ 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,
ScrollX: pointer.ScrollRange{Min: -20},
ScrollY: pointer.ScrollRange{Min: -40},
Target: t,
Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
}
}
events(&r, -1, f3(handler3))
@@ -1086,33 +1085,6 @@ 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.
type offer struct {
data string
+36 -35
View File
@@ -35,9 +35,10 @@ 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
scratchFilter keyFilter
filter keyFilter
nextFilter keyFilter
processedFilter keyFilter
scratchFilter keyFilter
}
cqueue clipboardQueue
// states is the list of pending state changes resulting from
@@ -60,7 +61,7 @@ type Router struct {
}
// Source implements the interface between a Router and user interface widgets.
// The zero-value Source is disabled.
// The value Source is disabled.
type Source struct {
r *Router
}
@@ -171,22 +172,22 @@ func (q *Router) Source() Source {
// Execute a command.
func (s Source) Execute(c Command) {
if !s.enabled() {
if !s.Enabled() {
return
}
s.r.execute(c)
}
// enabled reports whether the source is enabled. Only enabled
// Enabled reports whether the source is enabled. Only enabled
// Sources deliver events and respond to commands.
func (s Source) enabled() bool {
func (s Source) Enabled() bool {
return s.r != nil
}
// Focused reports whether tag is focused, according to the most recent
// [key.FocusEvent] delivered.
func (s Source) Focused(tag event.Tag) bool {
if !s.enabled() {
if !s.Enabled() {
return false
}
return s.r.state().keyState.focus == tag
@@ -194,7 +195,7 @@ func (s Source) Focused(tag event.Tag) bool {
// Event returns the next event that matches at least one of filters.
func (s Source) Event(filters ...event.Filter) (event.Event, bool) {
if !s.enabled() {
if !s.Enabled() {
return nil, false
}
return s.r.Event(filters...)
@@ -274,29 +275,28 @@ func (q *Router) Event(filters ...event.Filter) (event.Event, bool) {
}
}
}
for i := range q.changes {
if q.deferring && i > 0 {
break
}
change := &q.changes[i]
for j, evt := range change.events {
match := false
switch e := evt.event.(type) {
case key.Event:
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
default:
for _, tf := range q.scratchFilters {
if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
match = true
break
if !q.deferring {
for i := range q.changes {
change := &q.changes[i]
for j, evt := range change.events {
match := false
switch e := evt.event.(type) {
case key.Event:
match = q.key.scratchFilter.Matches(change.state.keyState.focus, e, false)
default:
for _, tf := range q.scratchFilters {
if evt.tag == tf.tag && tf.filter.Matches(evt.event) {
match = true
break
}
}
}
}
if match {
change.events = append(change.events[:j], change.events[j+1:]...)
// Fast forward state to last matched.
q.collapseState(i)
return evt.event, true
if match {
change.events = append(change.events[:j], change.events[j+1:]...)
// Fast forward state to last matched.
q.collapseState(i)
return evt.event, true
}
}
}
}
@@ -304,6 +304,7 @@ 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
}
@@ -314,15 +315,15 @@ func (q *Router) collapseState(idx int) {
}
first := &q.changes[0]
first.state = q.changes[idx].state
for _, ch := range q.changes[1 : idx+1] {
first.events = append(first.events, ch.events...)
for i := 1; i <= idx; i++ {
first.events = append(first.events, q.changes[i].events...)
}
q.changes = append(q.changes[:1], q.changes[idx+1:]...)
}
// Frame completes the current frame and starts a new with the
// handlers from the frame argument. Remaining events are discarded,
// unless they were deferred by a command.
// Frame replaces the declared handlers from the supplied
// operation list. The text input state, wakeup time and whether
// there are active profile handlers is also saved.
func (q *Router) Frame(frame *op.Ops) {
var remaining []event.Event
if n := len(q.changes); n > 0 {
+2 -2
View File
@@ -37,11 +37,11 @@ For example:
var h1, h2 *Handler
area := clip.Rect(...).Push(ops)
event.Op(Ops, h1)
event.Op{Tag: h1}.Add(Ops)
area.Pop()
area := clip.Rect(...).Push(ops)
event.Op(Ops, h2)
event.Op{Tag: h2}.Add(ops)
area.Pop()
implies a tree of two inner nodes, each with one pointer handler attached.
+6 -19
View File
@@ -3,6 +3,7 @@
package pointer
import (
"image"
"strings"
"time"
@@ -60,19 +61,12 @@ type Filter struct {
Target event.Tag
// Kinds is a bitwise-or of event types to match.
Kinds Kind
// ScrollX and ScrollY constrain the range of scrolling events delivered
// to Target. Specifically, any Event e delivered to Tag will satisfy
// ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy
//
// 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
// 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
}
// GrabCmd requests a pointer grab on the pointer identified by ID.
@@ -225,13 +219,6 @@ 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)
+3 -17
View File
@@ -5,7 +5,6 @@ package layout
import (
"time"
"gioui.org/io/event"
"gioui.org/io/input"
"gioui.org/io/system"
"gioui.org/op"
@@ -29,7 +28,6 @@ type Context struct {
// Interested users must look up and populate these values manually.
Locale system.Locale
disabled bool
input.Source
*op.Ops
}
@@ -44,21 +42,9 @@ func (c Context) Sp(v unit.Sp) int {
return c.Metric.Sp(v)
}
func (c Context) Event(filters ...event.Filter) (event.Event, bool) {
if c.disabled {
return nil, false
}
return c.Source.Event(filters...)
}
// Enabled reports whether this context is enabled. Disabled contexts
// don't report events.
func (c Context) Enabled() bool {
return !c.disabled
}
// Disabled returns a copy of this context that don't deliver any events.
// Disabled returns a copy of this context with a disabled Source,
// blocking widgets from changing its state and receiving events.
func (c Context) Disabled() Context {
c.disabled = true
c.Source = input.Source{}
return c
}
+4 -6
View File
@@ -7,7 +7,6 @@ import (
"math"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/op"
"gioui.org/op/clip"
)
@@ -159,12 +158,11 @@ func (l *List) update(gtx Context) {
max = 0
}
}
xrange := pointer.ScrollRange{Min: min, Max: max}
yrange := pointer.ScrollRange{}
if l.Axis == Vertical {
xrange, yrange = yrange, xrange
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
}
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), xrange, yrange)
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange)
l.scrollDelta = d
l.Position.Offset += d
}
-3
View File
@@ -138,9 +138,6 @@ 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,
+91 -44
View File
@@ -4,7 +4,6 @@ package text
import (
"bytes"
"fmt"
"image"
"io"
"log"
@@ -12,9 +11,10 @@ import (
"github.com/go-text/typesetting/di"
"github.com/go-text/typesetting/font"
gotextot "github.com/go-text/typesetting/font/opentype"
"github.com/go-text/typesetting/fontscan"
"github.com/go-text/typesetting/language"
"github.com/go-text/typesetting/opentype/api"
"github.com/go-text/typesetting/opentype/api/metadata"
"github.com/go-text/typesetting/shaping"
"golang.org/x/exp/slices"
"golang.org/x/image/math/fixed"
@@ -200,7 +200,7 @@ type runLayout struct {
// Direction is the layout direction of the glyphs.
Direction system.TextDirection
// face is the font face that the ID of each Glyph in the Layout refers to.
face *font.Face
face font.Face
// truncator indicates that this run is a text truncator standing in for remaining
// text.
truncator bool
@@ -210,8 +210,8 @@ type runLayout struct {
type shaperImpl struct {
// Fields for tracking fonts/faces.
fontMap *fontscan.FontMap
faces []*font.Face
faceToIndex map[*font.Font]int
faces []font.Face
faceToIndex map[font.Font]int
faceMeta []giofont.Font
defaultFaces []string
logger interface {
@@ -253,7 +253,7 @@ func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
var shaper shaperImpl
shaper.logger = newDebugLogger()
shaper.fontMap = fontscan.NewFontMap(shaper.logger)
shaper.faceToIndex = make(map[*font.Font]int)
shaper.faceToIndex = make(map[font.Font]int)
if systemFonts {
str, err := os.UserCacheDir()
if err != nil {
@@ -276,12 +276,11 @@ func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
// 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.
func (s *shaperImpl) Load(f FontFace) {
desc := opentype.FontToDescription(f.Font)
s.fontMap.AddFace(f.Face.Face(), fontscan.Location{File: fmt.Sprint(desc)}, desc)
s.fontMap.AddFace(f.Face.Face(), opentype.FontToDescription(f.Font))
s.addFace(f.Face.Face(), f.Font)
}
func (s *shaperImpl) addFace(f *font.Face, md giofont.Font) {
func (s *shaperImpl) addFace(f font.Face, md giofont.Font) {
if _, ok := s.faceToIndex[f.Font]; ok {
return
}
@@ -376,11 +375,11 @@ func (s *shaperImpl) splitBidi(input shaping.Input) []shaping.Input {
// ResolveFace allows shaperImpl to implement shaping.FontMap, wrapping its fontMap
// field and ensuring that any faces loaded as part of the search are registered with
// ids so that they can be referred to by a GlyphID.
func (s *shaperImpl) ResolveFace(r rune) *font.Face {
func (s *shaperImpl) ResolveFace(r rune) font.Face {
face := s.fontMap.ResolveFace(r)
if face != nil {
family, aspect := s.fontMap.FontMetadata(face.Font)
md := opentype.DescriptionToFont(font.Description{
md := opentype.DescriptionToFont(metadata.Description{
Family: family,
Aspect: aspect,
})
@@ -490,11 +489,9 @@ func wrapPolicyToGoText(p WrapPolicy) shaping.LineBreakPolicy {
// shapeAndWrapText invokes the text shaper and returns wrapped lines in the shaper's native format.
func (s *shaperImpl) shapeAndWrapText(params Parameters, txt []rune) (_ []shaping.Line, truncated int) {
wc := shaping.WrapConfig{
Direction: mapDirection(params.Locale.Direction),
TruncateAfterLines: params.MaxLines,
TextContinues: params.forceTruncate,
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
DisableTrailingWhitespaceTrim: params.DisableSpaceTrim,
TruncateAfterLines: params.MaxLines,
TextContinues: params.forceTruncate,
BreakPolicy: wrapPolicyToGoText(params.WrapPolicy),
}
families := s.defaultFaces
if params.Font.Typeface != "" {
@@ -664,7 +661,7 @@ func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
scaleFactor := fixedToFloat(ppem) / float32(face.Upem())
glyphData := face.GlyphData(gid)
switch glyphData := glyphData.(type) {
case font.GlyphOutline:
case api.GlyphOutline:
outline := glyphData
// Move to glyph position.
pos := f32.Point{
@@ -679,9 +676,9 @@ func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
for _, fseg := range outline.Segments {
nargs := 1
switch fseg.Op {
case gotextot.SegmentOpQuadTo:
case api.SegmentOpQuadTo:
nargs = 2
case gotextot.SegmentOpCubeTo:
case api.SegmentOpCubeTo:
nargs = 3
}
var args [3]f32.Point
@@ -696,13 +693,13 @@ func (s *shaperImpl) Shape(pathOps *op.Ops, gs []Glyph) clip.PathSpec {
}
}
switch fseg.Op {
case gotextot.SegmentOpMoveTo:
case api.SegmentOpMoveTo:
builder.Move(args[0])
case gotextot.SegmentOpLineTo:
case api.SegmentOpLineTo:
builder.Line(args[0])
case gotextot.SegmentOpQuadTo:
case api.SegmentOpQuadTo:
builder.Quad(args[0], args[1])
case gotextot.SegmentOpCubeTo:
case api.SegmentOpCubeTo:
builder.Cube(args[0], args[1], args[2])
default:
panic("unsupported segment op")
@@ -743,16 +740,16 @@ func (s *shaperImpl) Bitmaps(ops *op.Ops, gs []Glyph) op.CallOp {
}
glyphData := face.GlyphData(gid)
switch glyphData := glyphData.(type) {
case font.GlyphBitmap:
case api.GlyphBitmap:
var imgOp paint.ImageOp
var imgSize image.Point
bitmapData, ok := s.bitmapGlyphCache.Get(g.ID)
if !ok {
var img image.Image
switch glyphData.Format {
case font.PNG, font.JPG, font.TIFF:
case api.PNG, api.JPG, api.TIFF:
img, _, _ = image.Decode(bytes.NewReader(glyphData.Data))
case font.BlackAndWhite:
case api.BlackAndWhite:
// This is a complex family of uncompressed bitmaps that don't seem to be
// very common in practice. We can try adding support later if needed.
fallthrough
@@ -808,7 +805,7 @@ type langConfig struct {
}
// toInput converts its parameters into a shaping.Input.
func toInput(face *font.Face, ppem fixed.Int26_6, lc langConfig, runes []rune) shaping.Input {
func toInput(face font.Face, ppem fixed.Int26_6, lc langConfig, runes []rune) shaping.Input {
var input shaping.Input
input.Direction = lc.Direction
input.Text = runes
@@ -868,14 +865,13 @@ func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph {
}
// toLine converts the output into a Line with the provided dominant text direction.
func toLine(faceToIndex map[*font.Font]int, o shaping.Line, dir system.TextDirection) line {
func toLine(faceToIndex map[font.Font]int, o shaping.Line, dir system.TextDirection) line {
if len(o) < 1 {
return line{}
}
line := line{
runs: make([]runLayout, len(o)),
direction: dir,
visualOrder: make([]int, len(o)),
runs: make([]runLayout, len(o)),
direction: dir,
}
maxSize := fixed.Int26_6(0)
for i := range o {
@@ -883,7 +879,7 @@ func toLine(faceToIndex map[*font.Font]int, o shaping.Line, dir system.TextDirec
if run.Size > maxSize {
maxSize = run.Size
}
var font *font.Font
var font font.Font
if run.Face != nil {
font = run.Face.Font
}
@@ -893,13 +889,11 @@ func toLine(faceToIndex map[*font.Font]int, o shaping.Line, dir system.TextDirec
Count: run.Runes.Count,
Offset: line.runeCount,
},
Direction: unmapDirection(run.Direction),
face: run.Face,
Advance: run.Advance,
PPEM: run.Size,
VisualPosition: int(run.VisualIndex),
Direction: unmapDirection(run.Direction),
face: run.Face,
Advance: run.Advance,
PPEM: run.Size,
}
line.visualOrder[run.VisualIndex] = i
line.runeCount += run.Runes.Count
line.width += run.Advance
if line.ascent < run.LineBounds.Ascent {
@@ -910,11 +904,64 @@ func toLine(faceToIndex map[*font.Font]int, o shaping.Line, dir system.TextDirec
}
}
line.lineHeight = maxSize
// Iterate and resolve the X of each run.
x := fixed.Int26_6(0)
for _, runIdx := range line.visualOrder {
line.runs[runIdx].X = x
x += line.runs[runIdx].Advance
}
computeVisualOrder(&line)
return line
}
// computeVisualOrder will populate the Line's VisualOrder field and the
// VisualPosition field of each element in Runs.
func computeVisualOrder(l *line) {
l.visualOrder = make([]int, len(l.runs))
const none = -1
bidiRangeStart := none
// visPos returns the visual position for an individual logically-indexed
// run in this line, taking only the line's overall text direction into
// account.
visPos := func(logicalIndex int) int {
if l.direction.Progression() == system.TowardOrigin {
return len(l.runs) - 1 - logicalIndex
}
return logicalIndex
}
// resolveBidi populated the line's VisualOrder fields for the elements in the
// half-open range [bidiRangeStart:bidiRangeEnd) indicating that those elements
// should be displayed in reverse-visual order.
resolveBidi := func(bidiRangeStart, bidiRangeEnd int) {
firstVisual := bidiRangeEnd - 1
// Just found the end of a bidi range.
for startIdx := bidiRangeStart; startIdx < bidiRangeEnd; startIdx++ {
pos := visPos(firstVisual)
l.runs[startIdx].VisualPosition = pos
l.visualOrder[pos] = startIdx
firstVisual--
}
bidiRangeStart = none
}
for runIdx, run := range l.runs {
if run.Direction.Progression() != l.direction.Progression() {
if bidiRangeStart == none {
bidiRangeStart = runIdx
}
continue
} else if bidiRangeStart != none {
// Just found the end of a bidi range.
resolveBidi(bidiRangeStart, runIdx)
bidiRangeStart = none
}
pos := visPos(runIdx)
l.runs[runIdx].VisualPosition = pos
l.visualOrder[pos] = runIdx
}
if bidiRangeStart != none {
// We ended iteration within a bidi segment, resolve it.
resolveBidi(bidiRangeStart, len(l.runs))
}
// Iterate and resolve the X of each run.
x := fixed.Int26_6(0)
for _, runIdx := range l.visualOrder {
l.runs[runIdx].X = x
x += l.runs[runIdx].Advance
}
}
+115
View File
@@ -3,6 +3,7 @@ package text
import (
"fmt"
"math"
"reflect"
"strconv"
"testing"
@@ -449,6 +450,120 @@ func TestToLine(t *testing.T) {
}
}
func TestComputeVisualOrder(t *testing.T) {
type testcase struct {
name string
input line
expectedVisualOrder []int
}
for _, tc := range []testcase{
{
name: "ltr",
input: line{
direction: system.LTR,
runs: []runLayout{
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.LTR},
},
},
expectedVisualOrder: []int{0, 1, 2},
},
{
name: "rtl",
input: line{
direction: system.RTL,
runs: []runLayout{
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.RTL},
},
},
expectedVisualOrder: []int{2, 1, 0},
},
{
name: "bidi-ltr",
input: line{
direction: system.LTR,
runs: []runLayout{
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.LTR},
},
},
expectedVisualOrder: []int{0, 3, 2, 1, 4},
},
{
name: "bidi-ltr-complex",
input: line{
direction: system.LTR,
runs: []runLayout{
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.RTL},
},
},
expectedVisualOrder: []int{1, 0, 2, 4, 3, 5, 7, 6, 8, 10, 9},
},
{
name: "bidi-rtl",
input: line{
direction: system.RTL,
runs: []runLayout{
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.RTL},
},
},
expectedVisualOrder: []int{4, 1, 2, 3, 0},
},
{
name: "bidi-rtl-complex",
input: line{
direction: system.RTL,
runs: []runLayout{
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.LTR},
{Direction: system.RTL},
{Direction: system.LTR},
{Direction: system.LTR},
},
},
expectedVisualOrder: []int{9, 10, 8, 6, 7, 5, 3, 4, 2, 0, 1},
},
} {
t.Run(tc.name, func(t *testing.T) {
computeVisualOrder(&tc.input)
if !reflect.DeepEqual(tc.input.visualOrder, tc.expectedVisualOrder) {
t.Errorf("expected visual order %v, got %v", tc.expectedVisualOrder, tc.input.visualOrder)
}
for i, visualIndex := range tc.input.visualOrder {
if pos := tc.input.runs[visualIndex].VisualPosition; pos != i {
t.Errorf("line.VisualOrder[%d]=%d, but line.Runs[%d].VisualPosition=%d", i, visualIndex, visualIndex, pos)
}
}
})
}
}
func FuzzLayout(f *testing.F) {
ltrFace, _ := opentype.Parse(goregular.TTF)
rtlFace, _ := opentype.Parse(nsareg.TTF)
+1 -14
View File
@@ -76,11 +76,6 @@ type Parameters struct {
// text with a MaxLines. It is unexported because this behavior only makes sense for the
// shaper to control when it iterates paragraphs of text.
forceTruncate bool
// DisableSpaceTrim prevents the width of the final whitespace glyph on a line from being zeroed.
// This is desirable for text editors (so that the whitespace can be selected), but is undesirable
// for ordinary display text.
DisableSpaceTrim bool
}
type FontFace = giofont.FontFace
@@ -204,15 +199,7 @@ func (f Flags) String() string {
type GlyphID uint64
// Shaper converts strings of text into glyphs that can be displayed. The same
// Shaper should not be used in different goroutines.
//
// The Shaper controls text layout and has a cache, implemented as a map, and
// so laying out text in two different goroutines can easily result in
// concurrent access to said map, resulting in a panic.
//
// Practically speaking, this means you should use different Shapers for
// different top-level windows.
// Shaper converts strings of text into glyphs that can be displayed.
type Shaper struct {
config struct {
disableSystemFonts bool
+23 -6
View File
@@ -11,11 +11,8 @@ import (
// Decorations handles the states of window decorations.
type Decorations struct {
// Maximized controls the look and behaviour of the maximize
// button. It is the user's responsibility to set Maximized
// according to the window state reported through [app.ConfigEvent].
Maximized bool
clicks map[int]*Clickable
maximized bool
}
// LayoutMove lays out the widget that makes a window movable.
@@ -43,6 +40,17 @@ func (d *Decorations) Clickable(action system.Action) *Clickable {
return click
}
// Perform updates the decorations as if the specified actions were
// performed by the user.
func (d *Decorations) Perform(actions system.Action) {
if actions&system.ActionMaximize != 0 {
d.maximized = true
}
if actions&(system.ActionUnmaximize|system.ActionMinimize|system.ActionFullscreen) != 0 {
d.maximized = false
}
}
// Update the state and return the set of actions activated by the user.
func (d *Decorations) Update(gtx layout.Context) system.Action {
var actions system.Action
@@ -52,12 +60,21 @@ func (d *Decorations) Update(gtx layout.Context) system.Action {
}
action := system.Action(1 << idx)
switch {
case action == system.ActionMaximize && d.Maximized:
case action == system.ActionMaximize && d.maximized:
action = system.ActionUnmaximize
case action == system.ActionUnmaximize && !d.Maximized:
case action == system.ActionUnmaximize && !d.maximized:
action = system.ActionMaximize
}
switch action {
case system.ActionMaximize, system.ActionUnmaximize:
d.maximized = !d.maximized
}
actions |= action
}
return actions
}
// Maximized returns whether the window is maximized.
func (d *Decorations) Maximized() bool {
return d.maximized
}
+13 -21
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 scrollX, scrollY pointer.ScrollRange
var scrollRange image.Rectangle
textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions()
if e.SingleLine {
scrollOffX := e.text.ScrollOff().X
scrollX.Min = min(-scrollOffX, 0)
scrollX.Max = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
scrollRange.Min.X = min(-scrollOffX, 0)
scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
} else {
scrollOffY := e.text.ScrollOff().Y
scrollY.Min = -scrollOffY
scrollY.Max = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
scrollRange.Min.Y = -scrollOffY
scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
}
sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollX, scrollY)
sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollRange)
var soff int
if e.SingleLine {
e.text.ScrollRel(sdist, 0)
@@ -289,9 +289,6 @@ 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
}
@@ -315,8 +312,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.MoveLineStart(selectionClear)
e.text.MoveLineEnd(selectionExtend)
e.text.MoveStart(selectionClear)
e.text.MoveEnd(selectionExtend)
e.dragging = false
}
}
@@ -377,8 +374,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.ModShortcut | key.ModShift},
key.Filter{Focus: e, Name: key.NameEnd, Optional: key.ModShortcut | 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.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}),
@@ -398,7 +395,7 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
case key.FocusEvent:
// Reset IME state.
e.ime.imeState = imeState{}
if ke.Focus && !e.ReadOnly {
if ke.Focus {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
case key.Event:
@@ -524,10 +521,6 @@ 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
}
@@ -589,9 +582,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.MoveLineStart(selAct)
e.text.MoveStart(selAct)
case key.NameEnd:
e.text.MoveLineEnd(selAct)
e.text.MoveEnd(selAct)
}
return nil, false
}
@@ -610,7 +603,6 @@ func (e *Editor) initBuffer() {
e.text.SingleLine = e.SingleLine
e.text.Mask = e.Mask
e.text.WrapPolicy = e.WrapPolicy
e.text.DisableSpaceTrim = true
}
// Update the state of the editor in response to input events. Update consumes editor
+17 -77
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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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.MoveLineEnd(selectionClear)
e.text.MoveEnd(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,10 +548,8 @@ const (
moveRune
moveLine
movePage
moveTextStart
moveTextEnd
moveLineStart
moveLineEnd
moveStart
moveEnd
moveCoord
moveWord
deleteWord
@@ -601,14 +599,10 @@ func TestEditorCaretConsistency(t *testing.T) {
e.text.MoveLines(int(distance), selectionClear)
case movePage:
e.text.MovePages(int(distance), 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 moveStart:
e.text.MoveStart(selectionClear)
case moveEnd:
e.text.MoveEnd(selectionClear)
case moveCoord:
e.text.MoveCoord(image.Pt(int(x), int(y)))
case moveWord:
@@ -885,7 +879,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 TestEditorSelectReflow(t *testing.T) {
func TestEditorSelect(t *testing.T) {
e := new(Editor)
e.SetText(`a 2 4 6 8 a
b 2 4 6 8 b
@@ -988,60 +982,6 @@ 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)
+47 -65
View File
@@ -2,18 +2,12 @@ package widget
import (
"bytes"
"image"
"image/png"
"io"
"os"
"testing"
nsareg "eliasnaur.com/font/noto/sans/arabic/regular"
"gioui.org/font"
"gioui.org/font/opentype"
"gioui.org/gpu/headless"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"golang.org/x/image/font/gofont/goregular"
"golang.org/x/image/math/fixed"
@@ -22,11 +16,11 @@ import (
// makePosTestText returns two bidi samples of shaped text at the given
// font size and wrapped to the given line width. The runeLimit, if nonzero,
// truncates the sample text to ensure shorter output for expensive tests.
func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (shaper *text.Shaper, source string, bidiLTR, bidiRTL []text.Glyph) {
func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (source string, bidiLTR, bidiRTL []text.Glyph) {
ltrFace, _ := opentype.Parse(goregular.TTF)
rtlFace, _ := opentype.Parse(nsareg.TTF)
shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
shaper := text.NewShaper(text.NoSystemFonts(), text.WithCollection([]font.FontFace{
{
Font: font.Font{Typeface: "LTR"},
Face: ltrFace,
@@ -64,7 +58,7 @@ func makePosTestText(fontSize, lineWidth int, alignOpposite bool) (shaper *text.
for g, ok := shaper.NextGlyph(); ok; g, ok = shaper.NextGlyph() {
bidiRTL = append(bidiRTL, g)
}
return shaper, bidiSource, bidiLTR, bidiRTL
return bidiSource, bidiLTR, bidiRTL
}
// makeAccountingTestText shapes text designed to stress rune accounting
@@ -266,7 +260,7 @@ func TestIndexPositionWhitespace(t *testing.T) {
func TestIndexPositionBidi(t *testing.T) {
fontSize := 16
lineWidth := fontSize * 10
shaper, _, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
_, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
type testcase struct {
name string
glyphs []text.Glyph
@@ -290,12 +284,13 @@ func TestIndexPositionBidi(t *testing.T) {
name: "bidi rtl",
glyphs: bidiRTLText,
expectedXs: []fixed.Int26_6{
// Line 0
5718, 6344, 6914, 7484, 7769, 8339, 8909, 9162, 9674, 10186, 5718, 5452, 4649, 4057, 3759, 3338, 3072, 2304, 1536, 768, 0,
// Line 1
9170, 8872, 8574, 8308, 6941, 7226, 7796, 8308, 6941, 6675, 6369, 5777, 4926, 4660, 3892, 3124, 2356, 1588, 1303, 788, 406, 0,
// Line 2
324, 614, 1184, 1754, 2039, 2313, 2883, 3395, 3907, 4192, 4762, 5332, 5902, 324, 0,
2665, 3291, 3861, 4431, 4716, 5286, 5856, 6109, 6621, 7133, 2665, 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.
8794, 8026, 7258, 6490, 5722, 5437, 4922, 4540, 4134, 3868, 0, 290, 860, 1430, 1715, 1989, 2559, 3071, 3583, // Positions on line 2.
324, 894, 1464, 2034, 324, 0, // Positions on line 3.
},
},
} {
@@ -342,35 +337,6 @@ func TestIndexPositionBidi(t *testing.T) {
printPositions(t, gi.positions)
if t.Failed() {
printGlyphs(t, tc.glyphs)
width := lineWidth
height := 100
cap := image.NewRGBA(image.Rect(0, 0, width, height))
w, _ := headless.NewWindow(width, height)
defer w.Release()
ops := new(op.Ops)
gtx := layout.Context{
Constraints: layout.Constraints{Max: image.Pt(width, height)},
Ops: ops,
}
it := textIterator{viewport: image.Rectangle{Max: image.Point{X: width, Y: height}}}
for _, g := range tc.glyphs {
it.processGlyph(g, true)
}
var glyphs [32]text.Glyph
line := glyphs[:0]
for _, g := range gi.glyphs {
var ok bool
if line, ok = it.paintGlyph(gtx, shaper, g, line); !ok {
break
}
}
w.Frame(ops)
w.Screenshot(cap)
b := new(bytes.Buffer)
_ = png.Encode(b, cap)
screenshotName := tc.name + ".png"
_ = os.WriteFile(screenshotName, b.Bytes(), 0o644)
t.Logf("wrote %q", screenshotName)
}
})
}
@@ -379,8 +345,8 @@ func TestIndexPositionBidi(t *testing.T) {
func TestIndexPositionLines(t *testing.T) {
fontSize := 16
lineWidth := fontSize * 10
_, source1, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
_, source2, bidiLTRTextOpp, bidiRTLTextOpp := makePosTestText(fontSize, lineWidth, true)
source1, bidiLTRText, bidiRTLText := makePosTestText(fontSize, lineWidth, false)
source2, bidiLTRTextOpp, bidiRTLTextOpp := makePosTestText(fontSize, lineWidth, true)
type testcase struct {
name string
source string
@@ -413,7 +379,7 @@ func TestIndexPositionLines(t *testing.T) {
xOff: fixed.Int26_6(0),
yOff: 60,
glyphs: 18,
width: fixed.Int26_6(8528),
width: fixed.Int26_6(8813),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
@@ -435,24 +401,32 @@ func TestIndexPositionLines(t *testing.T) {
{
xOff: fixed.Int26_6(0),
yOff: 22,
glyphs: 20,
width: fixed.Int26_6(10186),
glyphs: 15,
width: fixed.Int26_6(7133),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(0),
yOff: 41,
glyphs: 19,
width: fixed.Int26_6(9170),
glyphs: 15,
width: fixed.Int26_6(7886),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(0),
yOff: 60,
glyphs: 13,
width: fixed.Int26_6(5902),
glyphs: 18,
width: fixed.Int26_6(8794),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(0),
yOff: 79,
glyphs: 4,
width: fixed.Int26_6(2034),
ascent: fixed.Int26_6(968),
descent: fixed.Int26_6(216),
},
@@ -480,10 +454,10 @@ func TestIndexPositionLines(t *testing.T) {
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(1712),
xOff: fixed.Int26_6(1427),
yOff: 60,
glyphs: 18,
width: fixed.Int26_6(8528),
width: fixed.Int26_6(8813),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
@@ -503,26 +477,34 @@ func TestIndexPositionLines(t *testing.T) {
glyphs: bidiRTLTextOpp,
expectedLines: []lineInfo{
{
xOff: fixed.Int26_6(54),
xOff: fixed.Int26_6(3107),
yOff: 22,
glyphs: 20,
width: fixed.Int26_6(10186),
glyphs: 15,
width: fixed.Int26_6(7133),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(1070),
xOff: fixed.Int26_6(2354),
yOff: 41,
glyphs: 19,
width: fixed.Int26_6(9170),
glyphs: 15,
width: fixed.Int26_6(7886),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(4338),
xOff: fixed.Int26_6(1446),
yOff: 60,
glyphs: 13,
width: fixed.Int26_6(5902),
glyphs: 18,
width: fixed.Int26_6(8794),
ascent: fixed.Int26_6(1407),
descent: fixed.Int26_6(756),
},
{
xOff: fixed.Int26_6(8206),
yOff: 79,
glyphs: 4,
width: fixed.Int26_6(2034),
ascent: fixed.Int26_6(968),
descent: fixed.Int26_6(216),
},
+1 -1
View File
@@ -75,7 +75,7 @@ func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimension
case system.ActionMinimize:
w = minimizeWindow
case system.ActionMaximize:
if d.Decorations.Maximized {
if d.Decorations.Maximized() {
w = maximizedWindow
} else {
w = maximizeWindow
-3
View File
@@ -54,9 +54,6 @@ 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)
}),
)
-3
View File
@@ -32,9 +32,6 @@ type Palette struct {
ContrastFg color.NRGBA
}
// Theme holds the general theme of an app or window. Different top-level
// windows should have different instances of Theme (with different Shapers;
// see the godoc for [text.Shaper]), though their other fields can be equal.
type Theme struct {
Shaper *text.Shaper
Palette
+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.MoveLineStart(selectionClear)
e.text.MoveLineEnd(selectionExtend)
e.text.MoveStart(selectionClear)
e.text.MoveEnd(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.MoveLineStart(selAct)
e.text.MoveStart(selAct)
case key.NameEnd:
e.text.MoveLineEnd(selAct)
e.text.MoveEnd(selAct)
}
}
+4 -30
View File
@@ -61,9 +61,6 @@ type textView struct {
Truncator string
// WrapPolicy configures how displayed text will be broken into lines.
WrapPolicy text.WrapPolicy
// DisableSpaceTrim configures whether trailing whitespace on a line will have its
// width zeroed. Set to true for editors, but false for non-editable text.
DisableSpaceTrim bool
// Mask replaces the visual display of each rune in the contents with the given rune.
// Newline characters are not masked. When non-zero, the unmasked contents
// are accessed by Len, Text, and SetText.
@@ -288,10 +285,6 @@ func (e *textView) Layout(gtx layout.Context, lt *text.Shaper, font font.Font, s
e.params.LineHeightScale = e.LineHeightScale
e.invalidate()
}
if e.DisableSpaceTrim != e.params.DisableSpaceTrim {
e.params.DisableSpaceTrim = e.DisableSpaceTrim
e.invalidate()
}
e.makeValid()
@@ -646,28 +639,9 @@ func (e *textView) MoveCaret(startDelta, endDelta int) {
e.caret.end = e.moveByGraphemes(e.caret.end, endDelta)
}
// 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
// MoveStart 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) MoveLineStart(selAct selectionAction) {
func (e *textView) MoveStart(selAct selectionAction) {
caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, 0)
e.caret.start = caret.runes
@@ -676,9 +650,9 @@ func (e *textView) MoveLineStart(selAct selectionAction) {
e.clampCursorToGraphemes()
}
// MoveLineEnd moves the caret to the end of the current line, ensuring that the resulting
// MoveEnd 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) MoveLineEnd(selAct selectionAction) {
func (e *textView) MoveEnd(selAct selectionAction) {
caret := e.closestToRune(e.caret.start)
caret = e.closestToLineCol(caret.lineCol.line, math.MaxInt)
e.caret.start = caret.runes