mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89d20c7d99 | |||
| 14bab8efae | |||
| f437aaf359 | |||
| cf5ae4aad9 | |||
| 8679f49fff | |||
| 83202263b9 | |||
| 7fde80e805 | |||
| e9d0619641 | |||
| 2e524200ab | |||
| cc477e9ca6 | |||
| 290b5fe821 | |||
| e9cb0b326d | |||
| 0e77a2b521 | |||
| 63550cc81e | |||
| 03c21dc1b5 |
@@ -47,6 +47,13 @@ type WndClassEx struct {
|
||||
HIconSm syscall.Handle
|
||||
}
|
||||
|
||||
type Margins struct {
|
||||
CxLeftWidth int32
|
||||
CxRightWidth int32
|
||||
CyTopHeight int32
|
||||
CyBottomHeight int32
|
||||
}
|
||||
|
||||
type Msg struct {
|
||||
Hwnd syscall.Handle
|
||||
Message uint32
|
||||
@@ -245,6 +252,7 @@ const (
|
||||
WM_MOUSEHWHEEL = 0x020E
|
||||
WM_NCACTIVATE = 0x0086
|
||||
WM_NCHITTEST = 0x0084
|
||||
WM_NCCALCSIZE = 0x0083
|
||||
WM_PAINT = 0x000F
|
||||
WM_QUIT = 0x0012
|
||||
WM_SETCURSOR = 0x0020
|
||||
@@ -379,6 +387,9 @@ var (
|
||||
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
|
||||
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
|
||||
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
|
||||
|
||||
dwmapi = syscall.NewLazySystemDLL("dwmapi")
|
||||
_DwmExtendFrameIntoClientArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
|
||||
)
|
||||
|
||||
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
|
||||
@@ -430,6 +441,14 @@ func DispatchMessage(m *Msg) {
|
||||
_DispatchMessage.Call(uintptr(unsafe.Pointer(m)))
|
||||
}
|
||||
|
||||
func DwmExtendFrameIntoClientArea(hwnd syscall.Handle, margins Margins) error {
|
||||
r, _, _ := _DwmExtendFrameIntoClientArea.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&margins)))
|
||||
if r != 0 {
|
||||
return fmt.Errorf("DwmExtendFrameIntoClientArea: %#x", r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EmptyClipboard() error {
|
||||
r, _, err := _EmptyClipboard.Call()
|
||||
if r == 0 {
|
||||
|
||||
+18
-11
@@ -6,7 +6,7 @@ package app
|
||||
#include <Foundation/Foundation.h>
|
||||
|
||||
__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
|
||||
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(uintptr_t handle);
|
||||
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void);
|
||||
__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl);
|
||||
__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
|
||||
__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
|
||||
@@ -42,7 +42,7 @@ static CFTypeRef newNSString(unichar *chars, NSUInteger length) {
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"runtime/cgo"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf16"
|
||||
@@ -70,6 +70,9 @@ type displayLink struct {
|
||||
running uint32
|
||||
}
|
||||
|
||||
// displayLinks maps CFTypeRefs to *displayLinks.
|
||||
var displayLinks sync.Map
|
||||
|
||||
var mainFuncs = make(chan func(), 1)
|
||||
|
||||
// runOnMain runs the function on the main thread.
|
||||
@@ -128,18 +131,18 @@ func NewDisplayLink(callback func()) (*displayLink, error) {
|
||||
states: make(chan bool),
|
||||
dids: make(chan uint64),
|
||||
}
|
||||
h := cgo.NewHandle(d)
|
||||
dl := C.gio_createDisplayLink(C.uintptr_t(h))
|
||||
dl := C.gio_createDisplayLink()
|
||||
if dl == 0 {
|
||||
return nil, errors.New("app: failed to create display link")
|
||||
}
|
||||
go d.run(dl, h)
|
||||
go d.run(dl)
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func (d *displayLink) run(dl C.CFTypeRef, h cgo.Handle) {
|
||||
func (d *displayLink) run(dl C.CFTypeRef) {
|
||||
defer C.gio_releaseDisplayLink(dl)
|
||||
defer h.Delete()
|
||||
displayLinks.Store(dl, d)
|
||||
defer displayLinks.Delete(dl)
|
||||
var stopTimer *time.Timer
|
||||
var tchan <-chan time.Time
|
||||
started := false
|
||||
@@ -200,10 +203,14 @@ func (d *displayLink) SetDisplayID(did uint64) {
|
||||
}
|
||||
|
||||
//export gio_onFrameCallback
|
||||
func gio_onFrameCallback(dl C.CFTypeRef, handle C.uintptr_t) {
|
||||
d := cgo.Handle(handle).Value().(*displayLink)
|
||||
if atomic.LoadUint32(&d.running) != 0 {
|
||||
d.callback()
|
||||
func gio_onFrameCallback(ref C.CFTypeRef) {
|
||||
d, exists := displayLinks.Load(ref)
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
dl := d.(*displayLink)
|
||||
if atomic.LoadUint32(&dl.running) != 0 {
|
||||
dl.callback()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-17
@@ -123,6 +123,9 @@ static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIE
|
||||
|
||||
@implementation GioView
|
||||
NSArray<UIKeyCommand *> *_keyCommands;
|
||||
+ (void)onFrameCallback:(CADisplayLink *)link {
|
||||
gio_onFrameCallback((__bridge CFTypeRef)link);
|
||||
}
|
||||
+ (Class)layerClass {
|
||||
return gio_layerClass();
|
||||
}
|
||||
@@ -227,23 +230,8 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface DisplayLinkHandle : NSObject {
|
||||
}
|
||||
@property uintptr_t handle;
|
||||
@end
|
||||
|
||||
@implementation DisplayLinkHandle {
|
||||
}
|
||||
|
||||
- (void)onFrameCallback:(CADisplayLink *)link {
|
||||
gio_onFrameCallback((__bridge CFTypeRef)link, _handle);
|
||||
}
|
||||
@end
|
||||
|
||||
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
|
||||
DisplayLinkHandle *h = [DisplayLinkHandle alloc];
|
||||
h.handle = handle;
|
||||
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:h selector:@selector(onFrameCallback:)];
|
||||
CFTypeRef gio_createDisplayLink(void) {
|
||||
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)];
|
||||
dl.paused = YES;
|
||||
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
|
||||
[dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
|
||||
|
||||
+5
-3
@@ -523,6 +523,8 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
|
||||
btn = pointer.ButtonPrimary
|
||||
case 1:
|
||||
btn = pointer.ButtonSecondary
|
||||
case 2:
|
||||
btn = pointer.ButtonTertiary
|
||||
}
|
||||
var typ pointer.Type
|
||||
switch cdir {
|
||||
@@ -788,13 +790,13 @@ func configFor(scale float32) unit.Metric {
|
||||
//export gio_onClose
|
||||
func gio_onClose(view C.CFTypeRef) {
|
||||
w := mustView(view)
|
||||
w.displayLink.Close()
|
||||
w.w.Event(ViewEvent{})
|
||||
deleteView(view)
|
||||
w.w.Event(system.DestroyEvent{})
|
||||
w.displayLink.Close()
|
||||
w.displayLink = nil
|
||||
deleteView(view)
|
||||
C.CFRelease(w.view)
|
||||
w.view = 0
|
||||
w.displayLink = nil
|
||||
}
|
||||
|
||||
//export gio_onHide
|
||||
|
||||
+15
-9
@@ -92,24 +92,30 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
||||
- (void)mouseUp:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_UP, 0, 0);
|
||||
}
|
||||
- (void)middleMouseDown:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
||||
}
|
||||
- (void)middleMouseUp:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_UP, 0, 0);
|
||||
}
|
||||
- (void)rightMouseDown:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
||||
}
|
||||
- (void)rightMouseUp:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_UP, 0, 0);
|
||||
}
|
||||
- (void)otherMouseDown:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
||||
}
|
||||
- (void)otherMouseUp:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_UP, 0, 0);
|
||||
}
|
||||
- (void)mouseMoved:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
||||
}
|
||||
- (void)mouseDragged:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
||||
}
|
||||
- (void)rightMouseDragged:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
||||
}
|
||||
- (void)otherMouseDragged:(NSEvent *)event {
|
||||
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
||||
}
|
||||
- (void)scrollWheel:(NSEvent *)event {
|
||||
CGFloat dx = -event.scrollingDeltaX;
|
||||
CGFloat dy = -event.scrollingDeltaY;
|
||||
@@ -193,14 +199,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
||||
static GioWindowDelegate *globalWindowDel;
|
||||
|
||||
static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) {
|
||||
gio_onFrameCallback(dl, (uintptr_t)handle);
|
||||
gio_onFrameCallback(dl);
|
||||
return kCVReturnSuccess;
|
||||
}
|
||||
|
||||
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
|
||||
CFTypeRef gio_createDisplayLink(void) {
|
||||
CVDisplayLinkRef dl;
|
||||
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
|
||||
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, (void *)(handle));
|
||||
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil);
|
||||
return dl;
|
||||
}
|
||||
|
||||
|
||||
+11
-1
@@ -94,7 +94,10 @@ type wlDisplay struct {
|
||||
|
||||
// Notification pipe fds.
|
||||
notify struct {
|
||||
read, write int
|
||||
read int
|
||||
|
||||
mu sync.Mutex
|
||||
write int
|
||||
}
|
||||
|
||||
repeat repeatState
|
||||
@@ -1442,6 +1445,11 @@ func (w *window) SetAnimating(anim bool) {
|
||||
// Wakeup wakes up the event loop through the notification pipe.
|
||||
func (d *wlDisplay) wakeup() {
|
||||
oneByte := make([]byte, 1)
|
||||
d.notify.mu.Lock()
|
||||
defer d.notify.mu.Unlock()
|
||||
if d.notify.write == 0 {
|
||||
return
|
||||
}
|
||||
if _, err := syscall.Write(d.notify.write, oneByte); err != nil && err != syscall.EAGAIN {
|
||||
panic(fmt.Errorf("failed to write to pipe: %v", err))
|
||||
}
|
||||
@@ -1820,10 +1828,12 @@ func newWLDisplay() (*wlDisplay, error) {
|
||||
}
|
||||
|
||||
func (d *wlDisplay) destroy() {
|
||||
d.notify.mu.Lock()
|
||||
if d.notify.write != 0 {
|
||||
syscall.Close(d.notify.write)
|
||||
d.notify.write = 0
|
||||
}
|
||||
d.notify.mu.Unlock()
|
||||
if d.notify.read != 0 {
|
||||
syscall.Close(d.notify.read)
|
||||
d.notify.read = 0
|
||||
|
||||
+22
-13
@@ -325,6 +325,11 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
// The system destroys the HWND for us.
|
||||
w.hwnd = 0
|
||||
windows.PostQuitMessage(0)
|
||||
case windows.WM_NCCALCSIZE:
|
||||
if !w.config.Decorated {
|
||||
// No client areas; we draw decorations ourselves.
|
||||
return 0
|
||||
}
|
||||
case windows.WM_PAINT:
|
||||
w.draw(true)
|
||||
case windows.WM_SIZE:
|
||||
@@ -446,23 +451,18 @@ func (w *window) hitTest(x, y int) uintptr {
|
||||
if w.config.Mode == Fullscreen {
|
||||
return windows.HTCLIENT
|
||||
}
|
||||
p := f32.Pt(float32(x), float32(y))
|
||||
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
|
||||
return windows.HTCAPTION
|
||||
}
|
||||
if w.config.Mode != Windowed {
|
||||
// Only windowed mode should allow resizing.
|
||||
return windows.HTCLIENT
|
||||
}
|
||||
// Check for resize handle before system actions; otherwise it can be impossible to
|
||||
// resize a custom-decorations window when the system move area is flush with the
|
||||
// edge of the window.
|
||||
top := y <= w.borderSize.Y
|
||||
bottom := y >= w.config.Size.Y-w.borderSize.Y
|
||||
left := x <= w.borderSize.X
|
||||
right := x >= w.config.Size.X-w.borderSize.X
|
||||
switch {
|
||||
default:
|
||||
fallthrough
|
||||
case !top && !bottom && !left && !right:
|
||||
return windows.HTCLIENT
|
||||
case top && left:
|
||||
return windows.HTTOPLEFT
|
||||
case top && right:
|
||||
@@ -480,6 +480,11 @@ func (w *window) hitTest(x, y int) uintptr {
|
||||
case right:
|
||||
return windows.HTRIGHT
|
||||
}
|
||||
p := f32.Pt(float32(x), float32(y))
|
||||
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
|
||||
return windows.HTCAPTION
|
||||
}
|
||||
return windows.HTCLIENT
|
||||
}
|
||||
|
||||
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
|
||||
@@ -669,9 +674,6 @@ func (w *window) Configure(options []Option) {
|
||||
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
|
||||
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
|
||||
style &^= winStyle
|
||||
if !w.config.Decorated {
|
||||
winStyle = 0
|
||||
}
|
||||
switch w.config.Mode {
|
||||
case Minimized:
|
||||
style |= winStyle
|
||||
@@ -695,9 +697,12 @@ func (w *window) Configure(options []Option) {
|
||||
// Set desired window size.
|
||||
wr.Right = wr.Left + width
|
||||
wr.Bottom = wr.Top + height
|
||||
// Convert from client size to window size.
|
||||
// Compute client size and position. Note that the client size is
|
||||
// equal to the window size when we are in control of decorations.
|
||||
r := wr
|
||||
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
|
||||
if w.config.Decorated {
|
||||
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
|
||||
}
|
||||
// Calculate difference between client and full window sizes.
|
||||
w.deltas.width = r.Right - wr.Right + wr.Left - r.Left
|
||||
w.deltas.height = r.Bottom - wr.Bottom + wr.Top - r.Top
|
||||
@@ -706,6 +711,10 @@ func (w *window) Configure(options []Option) {
|
||||
y = wr.Top
|
||||
width = r.Right - r.Left
|
||||
height = r.Bottom - r.Top
|
||||
if !w.config.Decorated {
|
||||
// Enable drop shadows when we draw decorations.
|
||||
windows.DwmExtendFrameIntoClientArea(w.hwnd, windows.Margins{-1, -1, -1, -1})
|
||||
}
|
||||
|
||||
case Fullscreen:
|
||||
mi := windows.GetMonitorInfo(w.hwnd)
|
||||
|
||||
@@ -62,6 +62,7 @@ func (c *Context) Release() {
|
||||
eglDestroyContext(c.disp, c.eglCtx.ctx)
|
||||
c.eglCtx = nil
|
||||
}
|
||||
eglTerminate(c.disp)
|
||||
c.disp = nilEGLDisplay
|
||||
}
|
||||
|
||||
|
||||
+37
-23
@@ -432,39 +432,42 @@ func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
|
||||
return id.id
|
||||
}
|
||||
|
||||
func (q *pointerQueue) ActionAt(pos f32.Point) (system.Action, bool) {
|
||||
for i := len(q.hitTree) - 1; i >= 0; i-- {
|
||||
n := &q.hitTree[i]
|
||||
hit, _ := q.hit(n.area, pos)
|
||||
if !hit {
|
||||
continue
|
||||
}
|
||||
func (q *pointerQueue) ActionAt(pos f32.Point) (action system.Action, hasAction bool) {
|
||||
q.hitTest(pos, func(n *hitNode) bool {
|
||||
area := q.areas[n.area]
|
||||
return area.action, area.action != 0
|
||||
}
|
||||
return 0, false
|
||||
if area.action != 0 {
|
||||
action = area.action
|
||||
hasAction = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return action, hasAction
|
||||
}
|
||||
|
||||
func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) {
|
||||
func (q *pointerQueue) SemanticAt(pos f32.Point) (semID SemanticID, hasSemID bool) {
|
||||
q.assignSemIDs()
|
||||
for i := len(q.hitTree) - 1; i >= 0; i-- {
|
||||
n := &q.hitTree[i]
|
||||
hit, _ := q.hit(n.area, pos)
|
||||
if !hit {
|
||||
continue
|
||||
}
|
||||
q.hitTest(pos, func(n *hitNode) bool {
|
||||
area := q.areas[n.area]
|
||||
if area.semantic.id != 0 {
|
||||
return area.semantic.id, true
|
||||
semID = area.semantic.id
|
||||
hasSemID = true
|
||||
return false
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
return true
|
||||
})
|
||||
return semID, hasSemID
|
||||
}
|
||||
|
||||
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
||||
// hitTest searches the hit tree for nodes matching pos. Any node matching pos will
|
||||
// have the onNode func invoked on it to allow the caller to extract whatever information
|
||||
// is necessary for further processing. onNode may return false to terminate the walk of
|
||||
// the hit tree, or true to continue. Providing this algorithm in this generic way
|
||||
// allows normal event routing and system action event routing to share the same traversal
|
||||
// logic even though they are interested in different aspects of hit nodes.
|
||||
func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode) bool) pointer.Cursor {
|
||||
// Track whether we're passing through hits.
|
||||
pass := true
|
||||
hits := q.scratch[:0]
|
||||
idx := len(q.hitTree) - 1
|
||||
cursor := pointer.CursorDefault
|
||||
for idx >= 0 {
|
||||
@@ -483,12 +486,23 @@ func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
||||
} else {
|
||||
idx = n.next
|
||||
}
|
||||
if !onNode(n) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return cursor
|
||||
}
|
||||
|
||||
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
||||
hits := q.scratch[:0]
|
||||
cursor := q.hitTest(pos, func(n *hitNode) bool {
|
||||
if n.tag != nil {
|
||||
if _, exists := q.handlers[n.tag]; exists {
|
||||
hits = addHandler(hits, n.tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
q.scratch = hits[:0]
|
||||
return hits, cursor
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/io/transfer"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
@@ -221,6 +222,43 @@ func TestPointerTypes(t *testing.T) {
|
||||
assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, pointer.Release)
|
||||
}
|
||||
|
||||
func TestPointerSystemAction(t *testing.T) {
|
||||
t.Run("simple", func(t *testing.T) {
|
||||
var ops op.Ops
|
||||
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||
system.ActionInputOp(system.ActionMove).Add(&ops)
|
||||
r1.Pop()
|
||||
|
||||
var r Router
|
||||
r.Frame(&ops)
|
||||
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
|
||||
})
|
||||
t.Run("covered by another clip", func(t *testing.T) {
|
||||
var ops op.Ops
|
||||
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||
system.ActionInputOp(system.ActionMove).Add(&ops)
|
||||
clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
|
||||
r1.Pop()
|
||||
|
||||
var r Router
|
||||
r.Frame(&ops)
|
||||
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
|
||||
})
|
||||
t.Run("uses topmost action op", func(t *testing.T) {
|
||||
var ops op.Ops
|
||||
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||
system.ActionInputOp(system.ActionMove).Add(&ops)
|
||||
r2 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
|
||||
system.ActionInputOp(system.ActionClose).Add(&ops)
|
||||
r2.Pop()
|
||||
r1.Pop()
|
||||
|
||||
var r Router
|
||||
r.Frame(&ops)
|
||||
assertActionAt(t, r, f32.Pt(50, 50), system.ActionClose)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPointerPriority(t *testing.T) {
|
||||
handler1 := new(int)
|
||||
handler2 := new(int)
|
||||
@@ -1231,6 +1269,17 @@ func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) {
|
||||
}
|
||||
}
|
||||
|
||||
// assertActionAt checks that the router has a system action of the expected type at point.
|
||||
func assertActionAt(t *testing.T, q Router, point f32.Point, expected system.Action) {
|
||||
t.Helper()
|
||||
action, ok := q.ActionAt(point)
|
||||
if !ok {
|
||||
t.Errorf("expected action %v at %v, got no action", expected, point)
|
||||
} else if action != expected {
|
||||
t.Errorf("expected action %v at %v, got %v", expected, point, action)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRouterAdd(b *testing.B) {
|
||||
// Set this to the number of overlapping handlers that you want to
|
||||
// evaluate performance for. Typical values for the example applications
|
||||
|
||||
+67
-67
@@ -36,7 +36,8 @@ type document struct {
|
||||
lines []line
|
||||
alignment Alignment
|
||||
// alignWidth is the width used when aligning text.
|
||||
alignWidth int
|
||||
alignWidth int
|
||||
unreadRuneCount int
|
||||
}
|
||||
|
||||
// append adds the lines of other to the end of l and ensures they
|
||||
@@ -52,6 +53,7 @@ func (l *document) reset() {
|
||||
l.lines = l.lines[:0]
|
||||
l.alignment = Start
|
||||
l.alignWidth = 0
|
||||
l.unreadRuneCount = 0
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
@@ -93,6 +95,55 @@ type line struct {
|
||||
yOffset int
|
||||
}
|
||||
|
||||
// insertTrailingSyntheticNewline adds a synthetic newline to the final logical run of the line
|
||||
// with the given shaping cluster index.
|
||||
func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) {
|
||||
// If there was a newline at the end of this paragraph, insert a synthetic glyph representing it.
|
||||
finalContentRun := len(l.runs) - 1
|
||||
// If there was a trailing newline update the rune counts to include
|
||||
// it on the last line of the paragraph.
|
||||
l.runeCount += 1
|
||||
l.runs[finalContentRun].Runes.Count += 1
|
||||
|
||||
syntheticGlyph := glyph{
|
||||
id: 0,
|
||||
clusterIndex: newLineClusterIdx,
|
||||
glyphCount: 0,
|
||||
runeCount: 1,
|
||||
xAdvance: 0,
|
||||
yAdvance: 0,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
}
|
||||
// Inset the synthetic newline glyph on the proper end of the run.
|
||||
if l.runs[finalContentRun].Direction.Progression() == system.FromOrigin {
|
||||
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, syntheticGlyph)
|
||||
} else {
|
||||
// Ensure capacity.
|
||||
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, glyph{})
|
||||
copy(l.runs[finalContentRun].Glyphs[1:], l.runs[finalContentRun].Glyphs)
|
||||
l.runs[finalContentRun].Glyphs[0] = syntheticGlyph
|
||||
}
|
||||
}
|
||||
|
||||
func (l *line) setTruncatedCount(truncatedCount int) {
|
||||
// If we've truncated the text with a truncator, adjust the rune counts within the
|
||||
// truncator to make it represent the truncated text.
|
||||
finalRunIdx := len(l.runs) - 1
|
||||
l.runs[finalRunIdx].truncator = true
|
||||
finalGlyphIdx := len(l.runs[finalRunIdx].Glyphs) - 1
|
||||
// The run represents all of the truncated text.
|
||||
l.runs[finalRunIdx].Runes.Count = truncatedCount
|
||||
// Only the final glyph represents any runes, and it represents all truncated text.
|
||||
for i := range l.runs[finalRunIdx].Glyphs {
|
||||
if i == finalGlyphIdx {
|
||||
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncatedCount
|
||||
} else {
|
||||
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Range describes the position and quantity of a range of text elements
|
||||
// within a larger slice. The unit is usually runes of unicode data or
|
||||
// glyphs of shaped font data.
|
||||
@@ -528,35 +579,21 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
||||
if hasNewline {
|
||||
txt = txt[:len(txt)-1]
|
||||
}
|
||||
truncatedNewline := false
|
||||
if hasNewline && len(txt) == 0 {
|
||||
params.forceTruncate = false
|
||||
// If we only have a newline, shape a space to get line metrics.
|
||||
ls, truncated = s.shapeAndWrapText(params, []rune{' '})
|
||||
if truncated > 0 {
|
||||
// Our space was truncated. Since our space didn't exist in any meaningful
|
||||
// capacity, ensure the truncated count is zeroed out.
|
||||
truncated = 0
|
||||
truncatedNewline = true
|
||||
} else {
|
||||
// We shaped a space to get proper line metrics, but we need to drop
|
||||
// the rune/glyph info since it isn't actually part of the text.
|
||||
ls[0][0].Glyphs = ls[0][0].Glyphs[:0]
|
||||
ls[0][0].Advance = 0
|
||||
ls[0][0].Runes.Count = 0
|
||||
}
|
||||
} else {
|
||||
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
|
||||
if params.MaxLines != 0 && hasNewline {
|
||||
// If we might end up truncating a trailing newline, we must insert the truncator symbol
|
||||
// on the final line (if we hit the limit).
|
||||
params.forceTruncate = true
|
||||
}
|
||||
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
|
||||
|
||||
didTruncate := truncated > 0 || truncatedNewline || (params.forceTruncate && params.MaxLines == len(ls))
|
||||
|
||||
if didTruncate && hasNewline {
|
||||
// We've truncated the newline, since it was at the end and we've truncated some amount of runes
|
||||
// before it.
|
||||
hasTruncator := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
|
||||
if hasTruncator && hasNewline {
|
||||
// We have a truncator at the end of the line, so the newline is logically
|
||||
// truncated as well.
|
||||
truncated++
|
||||
hasNewline = false
|
||||
}
|
||||
|
||||
// Convert to Lines.
|
||||
textLines := make([]line, len(ls))
|
||||
maxHeight := fixed.Int26_6(0)
|
||||
@@ -565,49 +602,12 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
||||
if otLine.lineHeight > maxHeight {
|
||||
maxHeight = otLine.lineHeight
|
||||
}
|
||||
isFinalLine := i == len(ls)-1
|
||||
if isFinalLine && hasNewline {
|
||||
// If there was a trailing newline update the rune counts to include
|
||||
// it on the last line of the paragraph.
|
||||
finalRunIdx := len(otLine.runs) - 1
|
||||
otLine.runeCount += 1
|
||||
otLine.runs[finalRunIdx].Runes.Count += 1
|
||||
|
||||
syntheticGlyph := glyph{
|
||||
id: 0,
|
||||
clusterIndex: len(txt),
|
||||
glyphCount: 0,
|
||||
runeCount: 1,
|
||||
xAdvance: 0,
|
||||
yAdvance: 0,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
if isFinalLine := i == len(ls)-1; isFinalLine {
|
||||
if hasNewline {
|
||||
otLine.insertTrailingSyntheticNewline(len(txt))
|
||||
}
|
||||
// Inset the synthetic newline glyph on the proper end of the run.
|
||||
if otLine.runs[finalRunIdx].Direction.Progression() == system.FromOrigin {
|
||||
otLine.runs[finalRunIdx].Glyphs = append(otLine.runs[finalRunIdx].Glyphs, syntheticGlyph)
|
||||
} else {
|
||||
// Ensure capacity.
|
||||
otLine.runs[finalRunIdx].Glyphs = append(otLine.runs[finalRunIdx].Glyphs, glyph{})
|
||||
copy(otLine.runs[finalRunIdx].Glyphs[1:], otLine.runs[finalRunIdx].Glyphs)
|
||||
otLine.runs[finalRunIdx].Glyphs[0] = syntheticGlyph
|
||||
}
|
||||
}
|
||||
if isFinalLine && didTruncate {
|
||||
// If we've truncated the text with a truncator, adjust the rune counts within the
|
||||
// truncator to make it represent the truncated text.
|
||||
finalRunIdx := len(otLine.runs) - 1
|
||||
otLine.runs[finalRunIdx].truncator = true
|
||||
finalGlyphIdx := len(otLine.runs[finalRunIdx].Glyphs) - 1
|
||||
// The run represents all of the truncated text.
|
||||
otLine.runs[finalRunIdx].Runes.Count = truncated
|
||||
// Only the final glyph represents any runes, and it represents all truncated text.
|
||||
for i := range otLine.runs[finalRunIdx].Glyphs {
|
||||
if i == finalGlyphIdx {
|
||||
otLine.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncated
|
||||
} else {
|
||||
otLine.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
|
||||
}
|
||||
if hasTruncator {
|
||||
otLine.setTruncatedCount(truncated)
|
||||
}
|
||||
}
|
||||
textLines[i] = otLine
|
||||
|
||||
+9
-7
@@ -245,8 +245,11 @@ func WithCollection(collection []FontFace) ShaperOption {
|
||||
}
|
||||
}
|
||||
|
||||
// NewShaper constructs a shaper with the provided collection of font faces
|
||||
// available.
|
||||
// NewShaper constructs a shaper with the provided options.
|
||||
//
|
||||
// NewShaper must be called after [app.NewWindow], unless the [NoSystemFonts]
|
||||
// option is specified. This is an unfortunate restriction caused by some platforms
|
||||
// such as Android.
|
||||
func NewShaper(options ...ShaperOption) *Shaper {
|
||||
l := &Shaper{}
|
||||
for _, opt := range options {
|
||||
@@ -351,11 +354,7 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
|
||||
unreadRunes++
|
||||
}
|
||||
}
|
||||
lastLineIdx := len(lines.lines) - 1
|
||||
lastRunIdx := len(lines.lines[lastLineIdx].runs) - 1
|
||||
lastGlyphIdx := len(lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs) - 1
|
||||
lines.lines[lastLineIdx].runs[lastRunIdx].Runes.Count += unreadRunes
|
||||
lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs[lastGlyphIdx].runeCount += unreadRunes
|
||||
l.txt.unreadRuneCount = unreadRunes
|
||||
}
|
||||
}
|
||||
l.txt.append(lines)
|
||||
@@ -505,6 +504,9 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
|
||||
}
|
||||
if endOfCluster {
|
||||
glyph.Flags |= FlagClusterBreak
|
||||
if run.truncator {
|
||||
glyph.Runes += l.txt.unreadRuneCount
|
||||
}
|
||||
} else {
|
||||
glyph.Runes = 0
|
||||
}
|
||||
|
||||
+36
-16
@@ -154,9 +154,11 @@ func TestWrappingForcedTruncation(t *testing.T) {
|
||||
// consistently and does not create spurious lines of text.
|
||||
func TestShapingNewlineHandling(t *testing.T) {
|
||||
type testcase struct {
|
||||
textInput string
|
||||
expectedLines int
|
||||
expectedGlyphs int
|
||||
textInput string
|
||||
expectedLines int
|
||||
expectedGlyphs int
|
||||
maxLines int
|
||||
expectedTruncated int
|
||||
}
|
||||
for _, tc := range []testcase{
|
||||
{textInput: "a\n", expectedLines: 1, expectedGlyphs: 3},
|
||||
@@ -165,21 +167,41 @@ func TestShapingNewlineHandling(t *testing.T) {
|
||||
{textInput: "\n", expectedLines: 1, expectedGlyphs: 2},
|
||||
{textInput: "\n\n", expectedLines: 2, expectedGlyphs: 3},
|
||||
{textInput: "\n\n\n", expectedLines: 3, expectedGlyphs: 4},
|
||||
{textInput: "\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 1},
|
||||
{textInput: "\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 2},
|
||||
{textInput: "\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 3},
|
||||
{textInput: "a\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 1},
|
||||
{textInput: "a\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 2},
|
||||
{textInput: "a\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 3},
|
||||
{textInput: "\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 2},
|
||||
{textInput: "\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 1},
|
||||
{textInput: "\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 2},
|
||||
{textInput: "a\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 3},
|
||||
{textInput: "a\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 1},
|
||||
{textInput: "a\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 2},
|
||||
} {
|
||||
t.Run(fmt.Sprintf("%q", tc.textInput), func(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%q-maxLines%d", tc.textInput, tc.maxLines), func(t *testing.T) {
|
||||
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||
collection := []FontFace{{Face: ltrFace}}
|
||||
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||
checkGlyphs := func() {
|
||||
glyphs := []Glyph{}
|
||||
runes := 0
|
||||
truncated := 0
|
||||
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
|
||||
glyphs = append(glyphs, g)
|
||||
runes += g.Runes
|
||||
if g.Flags&FlagTruncator == 0 {
|
||||
runes += g.Runes
|
||||
} else {
|
||||
truncated += g.Runes
|
||||
}
|
||||
}
|
||||
if expected := len([]rune(tc.textInput)); expected != runes {
|
||||
if expected := len([]rune(tc.textInput)) - tc.expectedTruncated; expected != runes {
|
||||
t.Errorf("expected %d runes, got %d", expected, runes)
|
||||
}
|
||||
if truncated != tc.expectedTruncated {
|
||||
t.Errorf("expected %d truncated runes, got %d", tc.expectedTruncated, truncated)
|
||||
}
|
||||
if len(glyphs) != tc.expectedGlyphs {
|
||||
t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs))
|
||||
}
|
||||
@@ -207,29 +229,27 @@ func TestShapingNewlineHandling(t *testing.T) {
|
||||
t.Errorf("expected paragraph start glyph to have cursor y")
|
||||
}
|
||||
}
|
||||
if count := strings.Count(tc.textInput, "\n"); found != count {
|
||||
if count := strings.Count(tc.textInput, "\n"); found != count && tc.maxLines == 0 {
|
||||
t.Errorf("expected %d paragraph breaks, found %d", count, found)
|
||||
} else if tc.maxLines > 0 && found > tc.maxLines {
|
||||
t.Errorf("expected %d paragraph breaks due to truncation, found %d", tc.maxLines, found)
|
||||
}
|
||||
}
|
||||
cache.LayoutString(Parameters{
|
||||
params := Parameters{
|
||||
Alignment: Middle,
|
||||
PxPerEm: fixed.I(10),
|
||||
MinWidth: 200,
|
||||
MaxWidth: 200,
|
||||
Locale: english,
|
||||
}, tc.textInput)
|
||||
MaxLines: tc.maxLines,
|
||||
}
|
||||
cache.LayoutString(params, tc.textInput)
|
||||
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
||||
t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount)
|
||||
}
|
||||
checkGlyphs()
|
||||
|
||||
cache.Layout(Parameters{
|
||||
Alignment: Middle,
|
||||
PxPerEm: fixed.I(10),
|
||||
MinWidth: 200,
|
||||
MaxWidth: 200,
|
||||
Locale: english,
|
||||
}, strings.NewReader(tc.textInput))
|
||||
cache.Layout(params, strings.NewReader(tc.textInput))
|
||||
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
||||
t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount)
|
||||
}
|
||||
|
||||
+19
-5
@@ -28,6 +28,7 @@ type Clickable struct {
|
||||
keyTag struct{}
|
||||
requestFocus bool
|
||||
focused bool
|
||||
pressedKey string
|
||||
}
|
||||
|
||||
// Click represents a click.
|
||||
@@ -178,17 +179,30 @@ func (b *Clickable) update(gtx layout.Context) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
b.focused = e.Focus
|
||||
if !b.focused {
|
||||
b.pressedKey = ""
|
||||
}
|
||||
case key.Event:
|
||||
if !b.focused || e.State != key.Release {
|
||||
if !b.focused {
|
||||
break
|
||||
}
|
||||
if e.Name != key.NameReturn && e.Name != key.NameSpace {
|
||||
break
|
||||
}
|
||||
b.clicks = append(b.clicks, Click{
|
||||
Modifiers: e.Modifiers,
|
||||
NumClicks: 1,
|
||||
})
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
b.pressedKey = e.Name
|
||||
case key.Release:
|
||||
if b.pressedKey != e.Name {
|
||||
break
|
||||
}
|
||||
// only register a key as a click if the key was pressed and released while this button was focused
|
||||
b.pressedKey = ""
|
||||
b.clicks = append(b.clicks, Click{
|
||||
Modifiers: e.Modifiers,
|
||||
NumClicks: 1,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package widget_test
|
||||
|
||||
import (
|
||||
"image"
|
||||
"testing"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/router"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
func TestClickable(t *testing.T) {
|
||||
var (
|
||||
ops op.Ops
|
||||
r router.Router
|
||||
b1 widget.Clickable
|
||||
b2 widget.Clickable
|
||||
)
|
||||
gtx := layout.NewContext(&ops, system.FrameEvent{Queue: &r})
|
||||
layout := func() {
|
||||
b1.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Dimensions{Size: image.Pt(100, 100)}
|
||||
})
|
||||
// buttons are on top of each other but we only use focus and keyevents, so this is fine
|
||||
b2.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Dimensions{Size: image.Pt(100, 100)}
|
||||
})
|
||||
}
|
||||
frame := func() {
|
||||
ops.Reset()
|
||||
layout()
|
||||
r.Frame(gtx.Ops)
|
||||
}
|
||||
// frame: request focus for button 1
|
||||
b1.Focus()
|
||||
frame()
|
||||
// frame: gain focus for button 1
|
||||
frame()
|
||||
if !b1.Focused() {
|
||||
t.Error("button 1 did not gain focus")
|
||||
}
|
||||
if b2.Focused() {
|
||||
t.Error("button 2 should not have focus")
|
||||
}
|
||||
// frame: press & release return
|
||||
r.Queue(
|
||||
key.Event{
|
||||
Name: key.NameReturn,
|
||||
State: key.Press,
|
||||
},
|
||||
key.Event{
|
||||
Name: key.NameReturn,
|
||||
State: key.Release,
|
||||
},
|
||||
)
|
||||
frame()
|
||||
if !b1.Clicked() {
|
||||
t.Error("button 1 did not get clicked when it got return press & release")
|
||||
}
|
||||
if b2.Clicked() {
|
||||
t.Error("button 2 got clicked when it did not have focus")
|
||||
}
|
||||
// frame: press return down
|
||||
r.Queue(
|
||||
key.Event{
|
||||
Name: key.NameReturn,
|
||||
State: key.Press,
|
||||
},
|
||||
)
|
||||
frame()
|
||||
if b1.Clicked() {
|
||||
t.Error("button 1 got clicked, even if it only got return press")
|
||||
}
|
||||
// frame: request focus for button 2
|
||||
b2.Focus()
|
||||
frame()
|
||||
// frame: gain focus for button 2
|
||||
frame()
|
||||
if b1.Focused() {
|
||||
t.Error("button 1 should not have focus")
|
||||
}
|
||||
if !b2.Focused() {
|
||||
t.Error("button 2 did not gain focus")
|
||||
}
|
||||
// frame: release return
|
||||
r.Queue(
|
||||
key.Event{
|
||||
Name: key.NameReturn,
|
||||
State: key.Release,
|
||||
},
|
||||
)
|
||||
frame()
|
||||
if b1.Clicked() {
|
||||
t.Error("button 1 got clicked, even if it had lost focus")
|
||||
}
|
||||
if b2.Clicked() {
|
||||
t.Error("button 2 should not have been clicked, as it only got return release")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user