mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-03 08:25:34 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b66dcc436c | |||
| 526db27c75 | |||
| 27193ae8e8 | |||
| 313c488ec3 | |||
| f30e936d9a | |||
| ae3bd2a1e1 | |||
| ae43d18ced | |||
| b4d93379c4 | |||
| b9654eb4eb | |||
| 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
|
HIconSm syscall.Handle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Margins struct {
|
||||||
|
CxLeftWidth int32
|
||||||
|
CxRightWidth int32
|
||||||
|
CyTopHeight int32
|
||||||
|
CyBottomHeight int32
|
||||||
|
}
|
||||||
|
|
||||||
type Msg struct {
|
type Msg struct {
|
||||||
Hwnd syscall.Handle
|
Hwnd syscall.Handle
|
||||||
Message uint32
|
Message uint32
|
||||||
@@ -69,6 +76,21 @@ type MinMaxInfo struct {
|
|||||||
PtMaxTrackSize Point
|
PtMaxTrackSize Point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NCCalcSizeParams struct {
|
||||||
|
Rgrc [3]Rect
|
||||||
|
LpPos *WindowPos
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowPos struct {
|
||||||
|
HWND syscall.Handle
|
||||||
|
HWNDInsertAfter syscall.Handle
|
||||||
|
x int32
|
||||||
|
y int32
|
||||||
|
cx int32
|
||||||
|
cy int32
|
||||||
|
flags uint32
|
||||||
|
}
|
||||||
|
|
||||||
type WindowPlacement struct {
|
type WindowPlacement struct {
|
||||||
length uint32
|
length uint32
|
||||||
flags uint32
|
flags uint32
|
||||||
@@ -245,6 +267,7 @@ const (
|
|||||||
WM_MOUSEHWHEEL = 0x020E
|
WM_MOUSEHWHEEL = 0x020E
|
||||||
WM_NCACTIVATE = 0x0086
|
WM_NCACTIVATE = 0x0086
|
||||||
WM_NCHITTEST = 0x0084
|
WM_NCHITTEST = 0x0084
|
||||||
|
WM_NCCALCSIZE = 0x0083
|
||||||
WM_PAINT = 0x000F
|
WM_PAINT = 0x000F
|
||||||
WM_QUIT = 0x0012
|
WM_QUIT = 0x0012
|
||||||
WM_SETCURSOR = 0x0020
|
WM_SETCURSOR = 0x0020
|
||||||
@@ -323,6 +346,7 @@ var (
|
|||||||
_DispatchMessage = user32.NewProc("DispatchMessageW")
|
_DispatchMessage = user32.NewProc("DispatchMessageW")
|
||||||
_EmptyClipboard = user32.NewProc("EmptyClipboard")
|
_EmptyClipboard = user32.NewProc("EmptyClipboard")
|
||||||
_GetWindowRect = user32.NewProc("GetWindowRect")
|
_GetWindowRect = user32.NewProc("GetWindowRect")
|
||||||
|
_GetClientRect = user32.NewProc("GetClientRect")
|
||||||
_GetClipboardData = user32.NewProc("GetClipboardData")
|
_GetClipboardData = user32.NewProc("GetClipboardData")
|
||||||
_GetDC = user32.NewProc("GetDC")
|
_GetDC = user32.NewProc("GetDC")
|
||||||
_GetDpiForWindow = user32.NewProc("GetDpiForWindow")
|
_GetDpiForWindow = user32.NewProc("GetDpiForWindow")
|
||||||
@@ -379,6 +403,9 @@ var (
|
|||||||
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
|
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
|
||||||
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
|
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
|
||||||
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
|
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
|
||||||
|
|
||||||
|
dwmapi = syscall.NewLazySystemDLL("dwmapi")
|
||||||
|
_DwmExtendFrameIntoClientArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
|
||||||
)
|
)
|
||||||
|
|
||||||
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
|
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
|
||||||
@@ -430,6 +457,14 @@ func DispatchMessage(m *Msg) {
|
|||||||
_DispatchMessage.Call(uintptr(unsafe.Pointer(m)))
|
_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 {
|
func EmptyClipboard() error {
|
||||||
r, _, err := _EmptyClipboard.Call()
|
r, _, err := _EmptyClipboard.Call()
|
||||||
if r == 0 {
|
if r == 0 {
|
||||||
@@ -444,6 +479,12 @@ func GetWindowRect(hwnd syscall.Handle) Rect {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetClientRect(hwnd syscall.Handle) Rect {
|
||||||
|
var r Rect
|
||||||
|
_GetClientRect.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&r)))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func GetClipboardData(format uint32) (syscall.Handle, error) {
|
func GetClipboardData(format uint32) (syscall.Handle, error) {
|
||||||
r, _, err := _GetClipboardData.Call(uintptr(format))
|
r, _, err := _GetClipboardData.Call(uintptr(format))
|
||||||
if r == 0 {
|
if r == 0 {
|
||||||
|
|||||||
@@ -242,6 +242,9 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
|||||||
if 'a' <= s && s <= 'z' {
|
if 'a' <= s && s <= 'z' {
|
||||||
return string(rune(s - 'a' + 'A')), true
|
return string(rune(s - 'a' + 'A')), true
|
||||||
}
|
}
|
||||||
|
if C.XKB_KEY_KP_0 <= s && s <= C.XKB_KEY_KP_9 {
|
||||||
|
return string(rune(s - C.XKB_KEY_KP_0 + '0')), true
|
||||||
|
}
|
||||||
if ' ' < s && s <= '~' {
|
if ' ' < s && s <= '~' {
|
||||||
return string(rune(s)), true
|
return string(rune(s)), true
|
||||||
}
|
}
|
||||||
@@ -255,8 +258,6 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
|||||||
n = key.NameRightArrow
|
n = key.NameRightArrow
|
||||||
case C.XKB_KEY_Return:
|
case C.XKB_KEY_Return:
|
||||||
n = key.NameReturn
|
n = key.NameReturn
|
||||||
case C.XKB_KEY_KP_Enter:
|
|
||||||
n = key.NameEnter
|
|
||||||
case C.XKB_KEY_Up:
|
case C.XKB_KEY_Up:
|
||||||
n = key.NameUpArrow
|
n = key.NameUpArrow
|
||||||
case C.XKB_KEY_Down:
|
case C.XKB_KEY_Down:
|
||||||
@@ -297,9 +298,9 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
|||||||
n = key.NameF11
|
n = key.NameF11
|
||||||
case C.XKB_KEY_F12:
|
case C.XKB_KEY_F12:
|
||||||
n = key.NameF12
|
n = key.NameF12
|
||||||
case C.XKB_KEY_Tab, C.XKB_KEY_KP_Tab, C.XKB_KEY_ISO_Left_Tab:
|
case C.XKB_KEY_Tab, C.XKB_KEY_ISO_Left_Tab:
|
||||||
n = key.NameTab
|
n = key.NameTab
|
||||||
case 0x20, C.XKB_KEY_KP_Space:
|
case 0x20:
|
||||||
n = key.NameSpace
|
n = key.NameSpace
|
||||||
case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R:
|
case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R:
|
||||||
n = key.NameCtrl
|
n = key.NameCtrl
|
||||||
@@ -309,6 +310,64 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
|
|||||||
n = key.NameAlt
|
n = key.NameAlt
|
||||||
case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R:
|
case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R:
|
||||||
n = key.NameSuper
|
n = key.NameSuper
|
||||||
|
|
||||||
|
case C.XKB_KEY_KP_Space:
|
||||||
|
n = key.NameSpace
|
||||||
|
case C.XKB_KEY_KP_Tab:
|
||||||
|
n = key.NameTab
|
||||||
|
case C.XKB_KEY_KP_Enter:
|
||||||
|
n = key.NameEnter
|
||||||
|
case C.XKB_KEY_KP_F1:
|
||||||
|
n = key.NameF1
|
||||||
|
case C.XKB_KEY_KP_F2:
|
||||||
|
n = key.NameF2
|
||||||
|
case C.XKB_KEY_KP_F3:
|
||||||
|
n = key.NameF3
|
||||||
|
case C.XKB_KEY_KP_F4:
|
||||||
|
n = key.NameF4
|
||||||
|
case C.XKB_KEY_KP_Home:
|
||||||
|
n = key.NameHome
|
||||||
|
case C.XKB_KEY_KP_Left:
|
||||||
|
n = key.NameLeftArrow
|
||||||
|
case C.XKB_KEY_KP_Up:
|
||||||
|
n = key.NameUpArrow
|
||||||
|
case C.XKB_KEY_KP_Right:
|
||||||
|
n = key.NameRightArrow
|
||||||
|
case C.XKB_KEY_KP_Down:
|
||||||
|
n = key.NameDownArrow
|
||||||
|
case C.XKB_KEY_KP_Prior:
|
||||||
|
// not supported
|
||||||
|
return "", false
|
||||||
|
case C.XKB_KEY_KP_Next:
|
||||||
|
// not supported
|
||||||
|
return "", false
|
||||||
|
case C.XKB_KEY_KP_End:
|
||||||
|
n = key.NameEnd
|
||||||
|
case C.XKB_KEY_KP_Begin:
|
||||||
|
n = key.NameHome
|
||||||
|
case C.XKB_KEY_KP_Insert:
|
||||||
|
// not supported
|
||||||
|
return "", false
|
||||||
|
case C.XKB_KEY_KP_Delete:
|
||||||
|
n = key.NameDeleteForward
|
||||||
|
case C.XKB_KEY_KP_Multiply:
|
||||||
|
n = "*"
|
||||||
|
case C.XKB_KEY_KP_Add:
|
||||||
|
n = "+"
|
||||||
|
case C.XKB_KEY_KP_Separator:
|
||||||
|
// not supported
|
||||||
|
return "", false
|
||||||
|
case C.XKB_KEY_KP_Subtract:
|
||||||
|
n = "-"
|
||||||
|
case C.XKB_KEY_KP_Decimal:
|
||||||
|
// TODO(dh): does a German keyboard layout also translate the numpad key to XKB_KEY_KP_DECIMAL? Because in
|
||||||
|
// German, the decimal is a comma, not a period.
|
||||||
|
n = "."
|
||||||
|
case C.XKB_KEY_KP_Divide:
|
||||||
|
n = "/"
|
||||||
|
case C.XKB_KEY_KP_Equal:
|
||||||
|
n = "="
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-11
@@ -6,7 +6,7 @@ package app
|
|||||||
#include <Foundation/Foundation.h>
|
#include <Foundation/Foundation.h>
|
||||||
|
|
||||||
__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
|
__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"))) void gio_releaseDisplayLink(CFTypeRef dl);
|
||||||
__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
|
__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
|
||||||
__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
|
__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
|
||||||
@@ -42,7 +42,7 @@ static CFTypeRef newNSString(unichar *chars, NSUInteger length) {
|
|||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"runtime/cgo"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf16"
|
"unicode/utf16"
|
||||||
@@ -70,6 +70,9 @@ type displayLink struct {
|
|||||||
running uint32
|
running uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// displayLinks maps CFTypeRefs to *displayLinks.
|
||||||
|
var displayLinks sync.Map
|
||||||
|
|
||||||
var mainFuncs = make(chan func(), 1)
|
var mainFuncs = make(chan func(), 1)
|
||||||
|
|
||||||
// runOnMain runs the function on the main thread.
|
// runOnMain runs the function on the main thread.
|
||||||
@@ -128,18 +131,18 @@ func NewDisplayLink(callback func()) (*displayLink, error) {
|
|||||||
states: make(chan bool),
|
states: make(chan bool),
|
||||||
dids: make(chan uint64),
|
dids: make(chan uint64),
|
||||||
}
|
}
|
||||||
h := cgo.NewHandle(d)
|
dl := C.gio_createDisplayLink()
|
||||||
dl := C.gio_createDisplayLink(C.uintptr_t(h))
|
|
||||||
if dl == 0 {
|
if dl == 0 {
|
||||||
return nil, errors.New("app: failed to create display link")
|
return nil, errors.New("app: failed to create display link")
|
||||||
}
|
}
|
||||||
go d.run(dl, h)
|
go d.run(dl)
|
||||||
return d, nil
|
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 C.gio_releaseDisplayLink(dl)
|
||||||
defer h.Delete()
|
displayLinks.Store(dl, d)
|
||||||
|
defer displayLinks.Delete(dl)
|
||||||
var stopTimer *time.Timer
|
var stopTimer *time.Timer
|
||||||
var tchan <-chan time.Time
|
var tchan <-chan time.Time
|
||||||
started := false
|
started := false
|
||||||
@@ -200,10 +203,14 @@ func (d *displayLink) SetDisplayID(did uint64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onFrameCallback
|
//export gio_onFrameCallback
|
||||||
func gio_onFrameCallback(dl C.CFTypeRef, handle C.uintptr_t) {
|
func gio_onFrameCallback(ref C.CFTypeRef) {
|
||||||
d := cgo.Handle(handle).Value().(*displayLink)
|
d, exists := displayLinks.Load(ref)
|
||||||
if atomic.LoadUint32(&d.running) != 0 {
|
if !exists {
|
||||||
d.callback()
|
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
|
@implementation GioView
|
||||||
NSArray<UIKeyCommand *> *_keyCommands;
|
NSArray<UIKeyCommand *> *_keyCommands;
|
||||||
|
+ (void)onFrameCallback:(CADisplayLink *)link {
|
||||||
|
gio_onFrameCallback((__bridge CFTypeRef)link);
|
||||||
|
}
|
||||||
+ (Class)layerClass {
|
+ (Class)layerClass {
|
||||||
return gio_layerClass();
|
return gio_layerClass();
|
||||||
}
|
}
|
||||||
@@ -227,23 +230,8 @@ NSArray<UIKeyCommand *> *_keyCommands;
|
|||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface DisplayLinkHandle : NSObject {
|
CFTypeRef gio_createDisplayLink(void) {
|
||||||
}
|
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)];
|
||||||
@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:)];
|
|
||||||
dl.paused = YES;
|
dl.paused = YES;
|
||||||
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
|
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
|
||||||
[dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
|
[dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
|
||||||
|
|||||||
+8
-6
@@ -365,11 +365,11 @@ func (w *window) Configure(options []Option) {
|
|||||||
case Minimized:
|
case Minimized:
|
||||||
C.unhideWindow(window)
|
C.unhideWindow(window)
|
||||||
case Maximized:
|
case Maximized:
|
||||||
|
if C.isWindowZoomed(window) != 0 {
|
||||||
|
C.zoomWindow(window)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
w.config.Mode = Windowed
|
w.config.Mode = Windowed
|
||||||
if C.isWindowZoomed(window) != 0 {
|
|
||||||
C.zoomWindow(window)
|
|
||||||
}
|
|
||||||
w.setTitle(prev, cnf)
|
w.setTitle(prev, cnf)
|
||||||
if prev.Size != cnf.Size {
|
if prev.Size != cnf.Size {
|
||||||
w.config.Size = cnf.Size
|
w.config.Size = cnf.Size
|
||||||
@@ -523,6 +523,8 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtn C.NSInteger, x, y, dx,
|
|||||||
btn = pointer.ButtonPrimary
|
btn = pointer.ButtonPrimary
|
||||||
case 1:
|
case 1:
|
||||||
btn = pointer.ButtonSecondary
|
btn = pointer.ButtonSecondary
|
||||||
|
case 2:
|
||||||
|
btn = pointer.ButtonTertiary
|
||||||
}
|
}
|
||||||
var typ pointer.Type
|
var typ pointer.Type
|
||||||
switch cdir {
|
switch cdir {
|
||||||
@@ -788,13 +790,13 @@ func configFor(scale float32) unit.Metric {
|
|||||||
//export gio_onClose
|
//export gio_onClose
|
||||||
func gio_onClose(view C.CFTypeRef) {
|
func gio_onClose(view C.CFTypeRef) {
|
||||||
w := mustView(view)
|
w := mustView(view)
|
||||||
w.displayLink.Close()
|
|
||||||
w.w.Event(ViewEvent{})
|
w.w.Event(ViewEvent{})
|
||||||
deleteView(view)
|
|
||||||
w.w.Event(system.DestroyEvent{})
|
w.w.Event(system.DestroyEvent{})
|
||||||
|
w.displayLink.Close()
|
||||||
|
w.displayLink = nil
|
||||||
|
deleteView(view)
|
||||||
C.CFRelease(w.view)
|
C.CFRelease(w.view)
|
||||||
w.view = 0
|
w.view = 0
|
||||||
w.displayLink = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//export gio_onHide
|
//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 {
|
- (void)mouseUp:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_UP, 0, 0);
|
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 {
|
- (void)rightMouseDown:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
handleMouse(self, event, MOUSE_DOWN, 0, 0);
|
||||||
}
|
}
|
||||||
- (void)rightMouseUp:(NSEvent *)event {
|
- (void)rightMouseUp:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_UP, 0, 0);
|
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 {
|
- (void)mouseMoved:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
||||||
}
|
}
|
||||||
- (void)mouseDragged:(NSEvent *)event {
|
- (void)mouseDragged:(NSEvent *)event {
|
||||||
handleMouse(self, event, MOUSE_MOVE, 0, 0);
|
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 {
|
- (void)scrollWheel:(NSEvent *)event {
|
||||||
CGFloat dx = -event.scrollingDeltaX;
|
CGFloat dx = -event.scrollingDeltaX;
|
||||||
CGFloat dy = -event.scrollingDeltaY;
|
CGFloat dy = -event.scrollingDeltaY;
|
||||||
@@ -193,14 +199,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
|||||||
static GioWindowDelegate *globalWindowDel;
|
static GioWindowDelegate *globalWindowDel;
|
||||||
|
|
||||||
static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) {
|
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;
|
return kCVReturnSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
|
CFTypeRef gio_createDisplayLink(void) {
|
||||||
CVDisplayLinkRef dl;
|
CVDisplayLinkRef dl;
|
||||||
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
|
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
|
||||||
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, (void *)(handle));
|
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil);
|
||||||
return dl;
|
return dl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -94,7 +94,10 @@ type wlDisplay struct {
|
|||||||
|
|
||||||
// Notification pipe fds.
|
// Notification pipe fds.
|
||||||
notify struct {
|
notify struct {
|
||||||
read, write int
|
read int
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
write int
|
||||||
}
|
}
|
||||||
|
|
||||||
repeat repeatState
|
repeat repeatState
|
||||||
@@ -1442,6 +1445,11 @@ func (w *window) SetAnimating(anim bool) {
|
|||||||
// Wakeup wakes up the event loop through the notification pipe.
|
// Wakeup wakes up the event loop through the notification pipe.
|
||||||
func (d *wlDisplay) wakeup() {
|
func (d *wlDisplay) wakeup() {
|
||||||
oneByte := make([]byte, 1)
|
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 {
|
if _, err := syscall.Write(d.notify.write, oneByte); err != nil && err != syscall.EAGAIN {
|
||||||
panic(fmt.Errorf("failed to write to pipe: %v", err))
|
panic(fmt.Errorf("failed to write to pipe: %v", err))
|
||||||
}
|
}
|
||||||
@@ -1820,10 +1828,12 @@ func newWLDisplay() (*wlDisplay, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *wlDisplay) destroy() {
|
func (d *wlDisplay) destroy() {
|
||||||
|
d.notify.mu.Lock()
|
||||||
if d.notify.write != 0 {
|
if d.notify.write != 0 {
|
||||||
syscall.Close(d.notify.write)
|
syscall.Close(d.notify.write)
|
||||||
d.notify.write = 0
|
d.notify.write = 0
|
||||||
}
|
}
|
||||||
|
d.notify.mu.Unlock()
|
||||||
if d.notify.read != 0 {
|
if d.notify.read != 0 {
|
||||||
syscall.Close(d.notify.read)
|
syscall.Close(d.notify.read)
|
||||||
d.notify.read = 0
|
d.notify.read = 0
|
||||||
|
|||||||
+63
-50
@@ -32,11 +32,6 @@ type ViewEvent struct {
|
|||||||
HWND uintptr
|
HWND uintptr
|
||||||
}
|
}
|
||||||
|
|
||||||
type winDeltas struct {
|
|
||||||
width int32
|
|
||||||
height int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type window struct {
|
type window struct {
|
||||||
hwnd syscall.Handle
|
hwnd syscall.Handle
|
||||||
hdc syscall.Handle
|
hdc syscall.Handle
|
||||||
@@ -55,7 +50,6 @@ type window struct {
|
|||||||
animating bool
|
animating bool
|
||||||
focused bool
|
focused bool
|
||||||
|
|
||||||
deltas winDeltas
|
|
||||||
borderSize image.Point
|
borderSize image.Point
|
||||||
config Config
|
config Config
|
||||||
}
|
}
|
||||||
@@ -192,22 +186,12 @@ func createNativeWindow() (*window, error) {
|
|||||||
// It reads the window style and size/position and updates w.config.
|
// It reads the window style and size/position and updates w.config.
|
||||||
// If anything has changed it emits a ConfigEvent to notify the application.
|
// If anything has changed it emits a ConfigEvent to notify the application.
|
||||||
func (w *window) update() {
|
func (w *window) update() {
|
||||||
r := windows.GetWindowRect(w.hwnd)
|
cr := windows.GetClientRect(w.hwnd)
|
||||||
size := image.Point{
|
w.config.Size = image.Point{
|
||||||
X: int(r.Right - r.Left - w.deltas.width),
|
X: int(cr.Right - cr.Left),
|
||||||
Y: int(r.Bottom - r.Top - w.deltas.height),
|
Y: int(cr.Bottom - cr.Top),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the window mode.
|
|
||||||
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
|
|
||||||
if style&windows.WS_OVERLAPPEDWINDOW == 0 {
|
|
||||||
size = image.Point{
|
|
||||||
X: int(r.Right - r.Left),
|
|
||||||
Y: int(r.Bottom - r.Top),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.config.Size = size
|
|
||||||
|
|
||||||
w.borderSize = image.Pt(
|
w.borderSize = image.Pt(
|
||||||
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
|
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
|
||||||
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
|
windows.GetSystemMetrics(windows.SM_CYSIZEFRAME),
|
||||||
@@ -325,6 +309,28 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
// The system destroys the HWND for us.
|
// The system destroys the HWND for us.
|
||||||
w.hwnd = 0
|
w.hwnd = 0
|
||||||
windows.PostQuitMessage(0)
|
windows.PostQuitMessage(0)
|
||||||
|
case windows.WM_NCCALCSIZE:
|
||||||
|
if w.config.Decorated {
|
||||||
|
// Let Windows handle decorations.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// No client areas; we draw decorations ourselves.
|
||||||
|
if wParam != 1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// lParam contains an NCCALCSIZE_PARAMS for us to adjust.
|
||||||
|
place := windows.GetWindowPlacement(w.hwnd)
|
||||||
|
if !place.IsMaximized() {
|
||||||
|
// Nothing do adjust.
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
// Adjust window position to avoid the extra padding in maximized
|
||||||
|
// state. See https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543.
|
||||||
|
// Note that trying to do the adjustment in WM_GETMINMAXINFO is ignored by Windows.
|
||||||
|
szp := (*windows.NCCalcSizeParams)(unsafe.Pointer(uintptr(lParam)))
|
||||||
|
mi := windows.GetMonitorInfo(w.hwnd)
|
||||||
|
szp.Rgrc[0] = mi.WorkArea
|
||||||
|
return 0
|
||||||
case windows.WM_PAINT:
|
case windows.WM_PAINT:
|
||||||
w.draw(true)
|
w.draw(true)
|
||||||
case windows.WM_SIZE:
|
case windows.WM_SIZE:
|
||||||
@@ -344,18 +350,26 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
}
|
}
|
||||||
case windows.WM_GETMINMAXINFO:
|
case windows.WM_GETMINMAXINFO:
|
||||||
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
|
mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam)))
|
||||||
|
var bw, bh int32
|
||||||
|
if w.config.Decorated {
|
||||||
|
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 {
|
if p := w.config.MinSize; p.X > 0 || p.Y > 0 {
|
||||||
mm.PtMinTrackSize = windows.Point{
|
mm.PtMinTrackSize = windows.Point{
|
||||||
X: int32(p.X) + w.deltas.width,
|
X: int32(p.X) + bw,
|
||||||
Y: int32(p.Y) + w.deltas.height,
|
Y: int32(p.Y) + bh,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if p := w.config.MaxSize; p.X > 0 || p.Y > 0 {
|
if p := w.config.MaxSize; p.X > 0 || p.Y > 0 {
|
||||||
mm.PtMaxTrackSize = windows.Point{
|
mm.PtMaxTrackSize = windows.Point{
|
||||||
X: int32(p.X) + w.deltas.width,
|
X: int32(p.X) + bw,
|
||||||
Y: int32(p.Y) + w.deltas.height,
|
Y: int32(p.Y) + bh,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 0
|
||||||
case windows.WM_SETCURSOR:
|
case windows.WM_SETCURSOR:
|
||||||
w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT
|
w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT
|
||||||
if w.cursorIn {
|
if w.cursorIn {
|
||||||
@@ -446,23 +460,18 @@ func (w *window) hitTest(x, y int) uintptr {
|
|||||||
if w.config.Mode == Fullscreen {
|
if w.config.Mode == Fullscreen {
|
||||||
return windows.HTCLIENT
|
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 {
|
if w.config.Mode != Windowed {
|
||||||
// Only windowed mode should allow resizing.
|
// Only windowed mode should allow resizing.
|
||||||
return windows.HTCLIENT
|
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
|
top := y <= w.borderSize.Y
|
||||||
bottom := y >= w.config.Size.Y-w.borderSize.Y
|
bottom := y >= w.config.Size.Y-w.borderSize.Y
|
||||||
left := x <= w.borderSize.X
|
left := x <= w.borderSize.X
|
||||||
right := x >= w.config.Size.X-w.borderSize.X
|
right := x >= w.config.Size.X-w.borderSize.X
|
||||||
switch {
|
switch {
|
||||||
default:
|
|
||||||
fallthrough
|
|
||||||
case !top && !bottom && !left && !right:
|
|
||||||
return windows.HTCLIENT
|
|
||||||
case top && left:
|
case top && left:
|
||||||
return windows.HTTOPLEFT
|
return windows.HTTOPLEFT
|
||||||
case top && right:
|
case top && right:
|
||||||
@@ -480,6 +489,11 @@ func (w *window) hitTest(x, y int) uintptr {
|
|||||||
case right:
|
case right:
|
||||||
return windows.HTRIGHT
|
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) {
|
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
|
||||||
@@ -669,9 +683,6 @@ func (w *window) Configure(options []Option) {
|
|||||||
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
|
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
|
||||||
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
|
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
|
||||||
style &^= winStyle
|
style &^= winStyle
|
||||||
if !w.config.Decorated {
|
|
||||||
winStyle = 0
|
|
||||||
}
|
|
||||||
switch w.config.Mode {
|
switch w.config.Mode {
|
||||||
case Minimized:
|
case Minimized:
|
||||||
style |= winStyle
|
style |= winStyle
|
||||||
@@ -684,28 +695,30 @@ func (w *window) Configure(options []Option) {
|
|||||||
showMode = windows.SW_SHOWMAXIMIZED
|
showMode = windows.SW_SHOWMAXIMIZED
|
||||||
|
|
||||||
case Windowed:
|
case Windowed:
|
||||||
windows.SetWindowText(w.hwnd, w.config.Title)
|
|
||||||
style |= winStyle
|
style |= winStyle
|
||||||
showMode = windows.SW_SHOWNORMAL
|
showMode = windows.SW_SHOWNORMAL
|
||||||
// Get target for client areaa size.
|
// Get target for client area size.
|
||||||
width = int32(w.config.Size.X)
|
width = int32(w.config.Size.X)
|
||||||
height = int32(w.config.Size.Y)
|
height = int32(w.config.Size.Y)
|
||||||
// Get the current window size and position.
|
// Get the current window size and position.
|
||||||
wr := windows.GetWindowRect(w.hwnd)
|
wr := windows.GetWindowRect(w.hwnd)
|
||||||
// Set desired window size.
|
|
||||||
wr.Right = wr.Left + width
|
|
||||||
wr.Bottom = wr.Top + height
|
|
||||||
// Convert from client size to window size.
|
|
||||||
r := wr
|
|
||||||
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
|
|
||||||
// Set new window size and position.
|
|
||||||
x = wr.Left
|
x = wr.Left
|
||||||
y = wr.Top
|
y = wr.Top
|
||||||
width = r.Right - r.Left
|
if w.config.Decorated {
|
||||||
height = r.Bottom - r.Top
|
// 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{
|
||||||
|
Right: width,
|
||||||
|
Bottom: height,
|
||||||
|
}
|
||||||
|
windows.AdjustWindowRectEx(&r, uint32(style), 0, dwExStyle)
|
||||||
|
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:
|
case Fullscreen:
|
||||||
mi := windows.GetMonitorInfo(w.hwnd)
|
mi := windows.GetMonitorInfo(w.hwnd)
|
||||||
@@ -718,7 +731,7 @@ func (w *window) Configure(options []Option) {
|
|||||||
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle)
|
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle)
|
||||||
windows.ShowWindow(w.hwnd, showMode)
|
windows.ShowWindow(w.hwnd, showMode)
|
||||||
|
|
||||||
w.w.Event(ConfigEvent{Config: w.config})
|
w.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) WriteClipboard(s string) {
|
func (w *window) WriteClipboard(s string) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
|
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
|
||||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
|
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
|
||||||
gioui.org/shader v1.0.6
|
gioui.org/shader v1.0.8
|
||||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372
|
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372
|
||||||
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
|
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
|
||||||
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
|
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8v
|
|||||||
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
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 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
|
||||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
|
||||||
gioui.org/shader v1.0.6 h1:cvZmU+eODFR2545X+/8XucgZdTtEjR3QWW6W65b0q5Y=
|
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
|
||||||
gioui.org/shader v1.0.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
|
||||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
|
github.com/go-text/typesetting v0.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 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/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
|
||||||
|
|||||||
+194
-43
@@ -68,22 +68,40 @@ type renderer struct {
|
|||||||
pather *pather
|
pather *pather
|
||||||
packer packer
|
packer packer
|
||||||
intersections packer
|
intersections packer
|
||||||
|
layers packer
|
||||||
|
layerFBOs fboSet
|
||||||
}
|
}
|
||||||
|
|
||||||
type drawOps struct {
|
type drawOps struct {
|
||||||
profile bool
|
profile bool
|
||||||
reader ops.Reader
|
reader ops.Reader
|
||||||
states []f32.Affine2D
|
states []f32.Affine2D
|
||||||
transStack []f32.Affine2D
|
transStack []f32.Affine2D
|
||||||
vertCache []byte
|
layers []opacityLayer
|
||||||
viewport image.Point
|
opacityStack []int
|
||||||
clear bool
|
vertCache []byte
|
||||||
clearColor f32color.RGBA
|
viewport image.Point
|
||||||
imageOps []imageOp
|
clear bool
|
||||||
pathOps []*pathOp
|
clearColor f32color.RGBA
|
||||||
pathOpCache []pathOp
|
imageOps []imageOp
|
||||||
qs quadSplitter
|
pathOps []*pathOp
|
||||||
pathCache *opCache
|
pathOpCache []pathOp
|
||||||
|
qs quadSplitter
|
||||||
|
pathCache *opCache
|
||||||
|
}
|
||||||
|
|
||||||
|
type opacityLayer struct {
|
||||||
|
opacity float32
|
||||||
|
parent int
|
||||||
|
// depth of the opacity stack. Layers of equal depth are
|
||||||
|
// independent and may be packed into one atlas.
|
||||||
|
depth int
|
||||||
|
// opStart and opEnd denote the range of drawOps.imageOps
|
||||||
|
// that belong to the layer.
|
||||||
|
opStart, opEnd int
|
||||||
|
// clip of the layer operations.
|
||||||
|
clip image.Rectangle
|
||||||
|
place placement
|
||||||
}
|
}
|
||||||
|
|
||||||
type drawState struct {
|
type drawState struct {
|
||||||
@@ -127,7 +145,12 @@ type imageOp struct {
|
|||||||
clip image.Rectangle
|
clip image.Rectangle
|
||||||
material material
|
material material
|
||||||
clipType clipType
|
clipType clipType
|
||||||
place placement
|
// place is either a placement in the path fbos or intersection fbos,
|
||||||
|
// depending on clipType.
|
||||||
|
place placement
|
||||||
|
// layerOps is the number of operations this
|
||||||
|
// operation replaces.
|
||||||
|
layerOps int
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeStrokeOp(data []byte) float32 {
|
func decodeStrokeOp(data []byte) float32 {
|
||||||
@@ -154,10 +177,12 @@ type material struct {
|
|||||||
// For materialTypeColor.
|
// For materialTypeColor.
|
||||||
color f32color.RGBA
|
color f32color.RGBA
|
||||||
// For materialTypeLinearGradient.
|
// For materialTypeLinearGradient.
|
||||||
color1 f32color.RGBA
|
color1 f32color.RGBA
|
||||||
color2 f32color.RGBA
|
color2 f32color.RGBA
|
||||||
|
opacity float32
|
||||||
// For materialTypeTexture.
|
// For materialTypeTexture.
|
||||||
data imageOpData
|
data imageOpData
|
||||||
|
tex driver.Texture
|
||||||
uvTrans f32.Affine2D
|
uvTrans f32.Affine2D
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +247,6 @@ func decodeLinearGradientOp(data []byte) linearGradientOpData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type clipType uint8
|
|
||||||
|
|
||||||
type resource interface {
|
type resource interface {
|
||||||
release()
|
release()
|
||||||
}
|
}
|
||||||
@@ -273,6 +296,9 @@ type blitUniforms struct {
|
|||||||
transform [4]float32
|
transform [4]float32
|
||||||
uvTransformR1 [4]float32
|
uvTransformR1 [4]float32
|
||||||
uvTransformR2 [4]float32
|
uvTransformR2 [4]float32
|
||||||
|
opacity float32
|
||||||
|
fbo float32
|
||||||
|
_ [2]float32
|
||||||
}
|
}
|
||||||
|
|
||||||
type colorUniforms struct {
|
type colorUniforms struct {
|
||||||
@@ -284,7 +310,7 @@ type gradientUniforms struct {
|
|||||||
color2 f32color.RGBA
|
color2 f32color.RGBA
|
||||||
}
|
}
|
||||||
|
|
||||||
type materialType uint8
|
type clipType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
clipTypeNone clipType = iota
|
clipTypeNone clipType = iota
|
||||||
@@ -292,6 +318,8 @@ const (
|
|||||||
clipTypeIntersection
|
clipTypeIntersection
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type materialType uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
materialColor materialType = iota
|
materialColor materialType = iota
|
||||||
materialLinearGradient
|
materialLinearGradient
|
||||||
@@ -391,6 +419,8 @@ func (g *gpu) frame(target RenderTarget) error {
|
|||||||
g.coverTimer.begin()
|
g.coverTimer.begin()
|
||||||
g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
|
g.renderer.uploadImages(g.cache, g.drawOps.imageOps)
|
||||||
g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
|
g.renderer.prepareDrawOps(g.cache, g.drawOps.imageOps)
|
||||||
|
g.drawOps.layers = g.renderer.packLayers(g.drawOps.layers)
|
||||||
|
g.renderer.drawLayers(g.cache, g.drawOps.layers, g.drawOps.imageOps)
|
||||||
d := driver.LoadDesc{
|
d := driver.LoadDesc{
|
||||||
ClearColor: g.drawOps.clearColor,
|
ClearColor: g.drawOps.clearColor,
|
||||||
}
|
}
|
||||||
@@ -400,7 +430,7 @@ func (g *gpu) frame(target RenderTarget) error {
|
|||||||
}
|
}
|
||||||
g.ctx.BeginRenderPass(defFBO, d)
|
g.ctx.BeginRenderPass(defFBO, d)
|
||||||
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
g.ctx.Viewport(0, 0, viewport.X, viewport.Y)
|
||||||
g.renderer.drawOps(g.cache, g.drawOps.imageOps)
|
g.renderer.drawOps(g.cache, false, image.Point{}, g.renderer.blitter.viewport, g.drawOps.imageOps)
|
||||||
g.coverTimer.end()
|
g.coverTimer.end()
|
||||||
g.ctx.EndRenderPass()
|
g.ctx.EndRenderPass()
|
||||||
g.cleanupTimer.begin()
|
g.cleanupTimer.begin()
|
||||||
@@ -464,15 +494,18 @@ func newRenderer(ctx driver.Device) *renderer {
|
|||||||
if cap := 8192; maxDim > cap {
|
if cap := 8192; maxDim > cap {
|
||||||
maxDim = cap
|
maxDim = cap
|
||||||
}
|
}
|
||||||
|
d := image.Pt(maxDim, maxDim)
|
||||||
|
|
||||||
r.packer.maxDims = image.Pt(maxDim, maxDim)
|
r.packer.maxDims = d
|
||||||
r.intersections.maxDims = image.Pt(maxDim, maxDim)
|
r.intersections.maxDims = d
|
||||||
|
r.layers.maxDims = d
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) release() {
|
func (r *renderer) release() {
|
||||||
r.pather.release()
|
r.pather.release()
|
||||||
r.blitter.release()
|
r.blitter.release()
|
||||||
|
r.layerFBOs.delete(r.ctx, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newBlitter(ctx driver.Device) *blitter {
|
func newBlitter(ctx driver.Device) *blitter {
|
||||||
@@ -747,8 +780,7 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
|
|||||||
ops = ops[:len(ops)-1]
|
ops = ops[:len(ops)-1]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()}
|
place, ok := r.packer.add(p.clip.Size())
|
||||||
place, ok := r.packer.add(sz)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
// The clip area is at most the entire screen. Hopefully no
|
// The clip area is at most the entire screen. Hopefully no
|
||||||
// screen is larger than GL_MAX_TEXTURE_SIZE.
|
// screen is larger than GL_MAX_TEXTURE_SIZE.
|
||||||
@@ -760,6 +792,83 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
|
|||||||
*pops = ops
|
*pops = ops
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *renderer) packLayers(layers []opacityLayer) []opacityLayer {
|
||||||
|
// Make every layer bounds contain nested layers; cull empty layers.
|
||||||
|
for i := len(layers) - 1; i >= 0; i-- {
|
||||||
|
l := layers[i]
|
||||||
|
if l.parent != -1 {
|
||||||
|
b := layers[l.parent].clip
|
||||||
|
layers[l.parent].clip = b.Union(l.clip)
|
||||||
|
}
|
||||||
|
if l.clip.Empty() {
|
||||||
|
layers = append(layers[:i], layers[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pack layers.
|
||||||
|
r.layers.clear()
|
||||||
|
depth := 0
|
||||||
|
for i := range layers {
|
||||||
|
l := &layers[i]
|
||||||
|
// Only layers of the same depth may be packed together.
|
||||||
|
if l.depth != depth {
|
||||||
|
r.layers.newPage()
|
||||||
|
}
|
||||||
|
place, ok := r.layers.add(l.clip.Size())
|
||||||
|
if !ok {
|
||||||
|
// The layer area is at most the entire screen. Hopefully no
|
||||||
|
// screen is larger than GL_MAX_TEXTURE_SIZE.
|
||||||
|
panic(fmt.Errorf("layer size %v is larger than maximum texture size %v", l.clip.Size(), r.layers.maxDims))
|
||||||
|
}
|
||||||
|
l.place = place
|
||||||
|
}
|
||||||
|
return layers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *renderer) drawLayers(cache *resourceCache, layers []opacityLayer, ops []imageOp) {
|
||||||
|
if len(r.layers.sizes) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fbo := -1
|
||||||
|
r.layerFBOs.resize(r.ctx, driver.TextureFormatSRGBA, r.layers.sizes)
|
||||||
|
for i := len(layers) - 1; i >= 0; i-- {
|
||||||
|
l := layers[i]
|
||||||
|
if fbo != l.place.Idx {
|
||||||
|
if fbo != -1 {
|
||||||
|
r.ctx.EndRenderPass()
|
||||||
|
r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
|
||||||
|
}
|
||||||
|
fbo = l.place.Idx
|
||||||
|
f := r.layerFBOs.fbos[fbo]
|
||||||
|
r.ctx.BeginRenderPass(f.tex, driver.LoadDesc{Action: driver.LoadActionClear})
|
||||||
|
}
|
||||||
|
v := image.Rectangle{
|
||||||
|
Min: l.place.Pos,
|
||||||
|
Max: l.place.Pos.Add(l.clip.Size()),
|
||||||
|
}
|
||||||
|
r.ctx.Viewport(v.Min.X, v.Min.Y, v.Max.X, v.Max.Y)
|
||||||
|
f := r.layerFBOs.fbos[fbo]
|
||||||
|
r.drawOps(cache, true, l.clip.Min.Mul(-1), l.clip.Size(), ops[l.opStart:l.opEnd])
|
||||||
|
sr := f32.FRect(v)
|
||||||
|
uvScale, uvOffset := texSpaceTransform(sr, f.size)
|
||||||
|
uvTrans := f32.Affine2D{}.Scale(f32.Point{}, uvScale).Offset(uvOffset)
|
||||||
|
// Replace layer ops with one textured op.
|
||||||
|
ops[l.opStart] = imageOp{
|
||||||
|
clip: l.clip,
|
||||||
|
material: material{
|
||||||
|
material: materialTexture,
|
||||||
|
tex: f.tex,
|
||||||
|
uvTrans: uvTrans,
|
||||||
|
opacity: l.opacity,
|
||||||
|
},
|
||||||
|
layerOps: l.opEnd - l.opStart - 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fbo != -1 {
|
||||||
|
r.ctx.EndRenderPass()
|
||||||
|
r.ctx.PrepareTexture(r.layerFBOs.fbos[fbo].tex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *drawOps) reset(viewport image.Point) {
|
func (d *drawOps) reset(viewport image.Point) {
|
||||||
d.profile = false
|
d.profile = false
|
||||||
d.viewport = viewport
|
d.viewport = viewport
|
||||||
@@ -768,6 +877,8 @@ func (d *drawOps) reset(viewport image.Point) {
|
|||||||
d.pathOpCache = d.pathOpCache[:0]
|
d.pathOpCache = d.pathOpCache[:0]
|
||||||
d.vertCache = d.vertCache[:0]
|
d.vertCache = d.vertCache[:0]
|
||||||
d.transStack = d.transStack[:0]
|
d.transStack = d.transStack[:0]
|
||||||
|
d.layers = d.layers[:0]
|
||||||
|
d.opacityStack = d.opacityStack[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *drawOps) collect(root *op.Ops, viewport image.Point) {
|
func (d *drawOps) collect(root *op.Ops, viewport image.Point) {
|
||||||
@@ -866,6 +977,27 @@ loop:
|
|||||||
state.t = d.transStack[n-1]
|
state.t = d.transStack[n-1]
|
||||||
d.transStack = d.transStack[:n-1]
|
d.transStack = d.transStack[:n-1]
|
||||||
|
|
||||||
|
case ops.TypePushOpacity:
|
||||||
|
opacity := ops.DecodeOpacity(encOp.Data)
|
||||||
|
parent := -1
|
||||||
|
depth := len(d.opacityStack)
|
||||||
|
if depth > 0 {
|
||||||
|
parent = d.opacityStack[depth-1]
|
||||||
|
}
|
||||||
|
lidx := len(d.layers)
|
||||||
|
d.layers = append(d.layers, opacityLayer{
|
||||||
|
opacity: opacity,
|
||||||
|
parent: parent,
|
||||||
|
depth: depth,
|
||||||
|
opStart: len(d.imageOps),
|
||||||
|
})
|
||||||
|
d.opacityStack = append(d.opacityStack, lidx)
|
||||||
|
case ops.TypePopOpacity:
|
||||||
|
n := len(d.opacityStack)
|
||||||
|
idx := d.opacityStack[n-1]
|
||||||
|
d.layers[idx].opEnd = len(d.imageOps)
|
||||||
|
d.opacityStack = d.opacityStack[:n-1]
|
||||||
|
|
||||||
case ops.TypeStroke:
|
case ops.TypeStroke:
|
||||||
quads.key.strokeWidth = decodeStrokeOp(encOp.Data)
|
quads.key.strokeWidth = decodeStrokeOp(encOp.Data)
|
||||||
|
|
||||||
@@ -958,7 +1090,7 @@ loop:
|
|||||||
mat := state.materialFor(bnd, off, partialTrans, bounds)
|
mat := state.materialFor(bnd, off, partialTrans, bounds)
|
||||||
|
|
||||||
rect := state.cpath == nil || state.cpath.rect
|
rect := state.cpath == nil || state.cpath.rect
|
||||||
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) {
|
if bounds.Min == (image.Point{}) && bounds.Max == d.viewport && rect && mat.opaque && (mat.material == materialColor) && len(d.opacityStack) == 0 {
|
||||||
// The image is a uniform opaque color and takes up the whole screen.
|
// The image is a uniform opaque color and takes up the whole screen.
|
||||||
// Scrap images up to and including this image and set clear color.
|
// Scrap images up to and including this image and set clear color.
|
||||||
d.imageOps = d.imageOps[:0]
|
d.imageOps = d.imageOps[:0]
|
||||||
@@ -971,6 +1103,15 @@ loop:
|
|||||||
clip: bounds,
|
clip: bounds,
|
||||||
material: mat,
|
material: mat,
|
||||||
}
|
}
|
||||||
|
if n := len(d.opacityStack); n > 0 {
|
||||||
|
idx := d.opacityStack[n-1]
|
||||||
|
lb := d.layers[idx].clip
|
||||||
|
if lb.Empty() {
|
||||||
|
d.layers[idx].clip = img.clip
|
||||||
|
} else {
|
||||||
|
d.layers[idx].clip = lb.Union(img.clip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
d.imageOps = append(d.imageOps, img)
|
d.imageOps = append(d.imageOps, img)
|
||||||
if clipData != nil {
|
if clipData != nil {
|
||||||
@@ -1000,7 +1141,9 @@ func expandPathOp(p *pathOp, clip image.Rectangle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle) material {
|
func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32.Affine2D, clip image.Rectangle) material {
|
||||||
var m material
|
m := material{
|
||||||
|
opacity: 1.,
|
||||||
|
}
|
||||||
switch d.matType {
|
switch d.matType {
|
||||||
case materialColor:
|
case materialColor:
|
||||||
m.material = materialColor
|
m.material = materialColor
|
||||||
@@ -1040,10 +1183,11 @@ func (d *drawState) materialFor(rect f32.Rectangle, off f32.Point, partTrans f32
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
|
func (r *renderer) uploadImages(cache *resourceCache, ops []imageOp) {
|
||||||
for _, img := range ops {
|
for i := range ops {
|
||||||
|
img := &ops[i]
|
||||||
m := img.material
|
m := img.material
|
||||||
if m.material == materialTexture {
|
if m.material == materialTexture {
|
||||||
r.texHandle(cache, m.data)
|
img.material.tex = r.texHandle(cache, m.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1053,10 +1197,10 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
|
|||||||
m := img.material
|
m := img.material
|
||||||
switch m.material {
|
switch m.material {
|
||||||
case materialTexture:
|
case materialTexture:
|
||||||
r.ctx.PrepareTexture(r.texHandle(cache, m.data))
|
r.ctx.PrepareTexture(m.tex)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fbo stencilFBO
|
var fbo FBO
|
||||||
switch img.clipType {
|
switch img.clipType {
|
||||||
case clipTypeNone:
|
case clipTypeNone:
|
||||||
continue
|
continue
|
||||||
@@ -1069,24 +1213,26 @@ func (r *renderer) prepareDrawOps(cache *resourceCache, ops []imageOp) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
|
func (r *renderer) drawOps(cache *resourceCache, isFBO bool, opOff image.Point, viewport image.Point, ops []imageOp) {
|
||||||
var coverTex driver.Texture
|
var coverTex driver.Texture
|
||||||
for _, img := range ops {
|
for i := 0; i < len(ops); i++ {
|
||||||
|
img := ops[i]
|
||||||
|
i += img.layerOps
|
||||||
m := img.material
|
m := img.material
|
||||||
switch m.material {
|
switch m.material {
|
||||||
case materialTexture:
|
case materialTexture:
|
||||||
r.ctx.BindTexture(0, r.texHandle(cache, m.data))
|
r.ctx.BindTexture(0, m.tex)
|
||||||
}
|
}
|
||||||
drc := img.clip
|
drc := img.clip.Add(opOff)
|
||||||
|
|
||||||
scale, off := clipSpaceTransform(drc, r.blitter.viewport)
|
scale, off := clipSpaceTransform(drc, viewport)
|
||||||
var fbo stencilFBO
|
var fbo FBO
|
||||||
switch img.clipType {
|
switch img.clipType {
|
||||||
case clipTypeNone:
|
case clipTypeNone:
|
||||||
p := r.blitter.pipelines[m.material]
|
p := r.blitter.pipelines[m.material]
|
||||||
r.ctx.BindPipeline(p.pipeline)
|
r.ctx.BindPipeline(p.pipeline)
|
||||||
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
||||||
r.blitter.blit(m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans)
|
r.blitter.blit(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.opacity, m.uvTrans)
|
||||||
continue
|
continue
|
||||||
case clipTypePath:
|
case clipTypePath:
|
||||||
fbo = r.pather.stenciler.cover(img.place.Idx)
|
fbo = r.pather.stenciler.cover(img.place.Idx)
|
||||||
@@ -1105,11 +1251,11 @@ func (r *renderer) drawOps(cache *resourceCache, ops []imageOp) {
|
|||||||
p := r.pather.coverer.pipelines[m.material]
|
p := r.pather.coverer.pipelines[m.material]
|
||||||
r.ctx.BindPipeline(p.pipeline)
|
r.ctx.BindPipeline(p.pipeline)
|
||||||
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
r.ctx.BindVertexBuffer(r.blitter.quadVerts, 0)
|
||||||
r.pather.cover(m.material, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
|
r.pather.cover(m.material, isFBO, m.color, m.color1, m.color2, scale, off, m.uvTrans, coverScale, coverOff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D) {
|
func (b *blitter) blit(mat materialType, fbo bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, opacity float32, uvTrans f32.Affine2D) {
|
||||||
p := b.pipelines[mat]
|
p := b.pipelines[mat]
|
||||||
b.ctx.BindPipeline(p.pipeline)
|
b.ctx.BindPipeline(p.pipeline)
|
||||||
var uniforms *blitUniforms
|
var uniforms *blitUniforms
|
||||||
@@ -1119,18 +1265,23 @@ func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.
|
|||||||
uniforms = &b.colUniforms.blitUniforms
|
uniforms = &b.colUniforms.blitUniforms
|
||||||
case materialTexture:
|
case materialTexture:
|
||||||
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
||||||
b.texUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
||||||
b.texUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
||||||
uniforms = &b.texUniforms.blitUniforms
|
uniforms = &b.texUniforms.blitUniforms
|
||||||
|
uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
||||||
|
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
||||||
case materialLinearGradient:
|
case materialLinearGradient:
|
||||||
b.linearGradientUniforms.color1 = col1
|
b.linearGradientUniforms.color1 = col1
|
||||||
b.linearGradientUniforms.color2 = col2
|
b.linearGradientUniforms.color2 = col2
|
||||||
|
|
||||||
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
t1, t2, t3, t4, t5, t6 := uvTrans.Elems()
|
||||||
b.linearGradientUniforms.blitUniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
|
||||||
b.linearGradientUniforms.blitUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
|
||||||
uniforms = &b.linearGradientUniforms.blitUniforms
|
uniforms = &b.linearGradientUniforms.blitUniforms
|
||||||
|
uniforms.uvTransformR1 = [4]float32{t1, t2, t3, 0}
|
||||||
|
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
||||||
}
|
}
|
||||||
|
uniforms.fbo = 0
|
||||||
|
if fbo {
|
||||||
|
uniforms.fbo = 1
|
||||||
|
}
|
||||||
|
uniforms.opacity = opacity
|
||||||
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
||||||
p.UploadUniforms(b.ctx)
|
p.UploadUniforms(b.ctx)
|
||||||
b.ctx.DrawArrays(0, 4)
|
b.ctx.DrawArrays(0, 4)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 334 B |
@@ -413,6 +413,22 @@ func TestGapsInPath(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOpacity(t *testing.T) {
|
||||||
|
run(t, func(ops *op.Ops) {
|
||||||
|
opc1 := paint.PushOpacity(ops, .3)
|
||||||
|
// Fill screen to exercize the glClear optimization.
|
||||||
|
paint.FillShape(ops, color.NRGBA{R: 255, A: 255}, clip.Rect{Max: image.Pt(1024, 1024)}.Op())
|
||||||
|
opc2 := paint.PushOpacity(ops, .6)
|
||||||
|
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(20, 10), Max: image.Pt(64, 128)}.Op())
|
||||||
|
opc2.Pop()
|
||||||
|
opc1.Pop()
|
||||||
|
opc3 := paint.PushOpacity(ops, .6)
|
||||||
|
paint.FillShape(ops, color.NRGBA{G: 255, A: 255}, clip.Rect{Min: image.Pt(50+20, 10), Max: image.Pt(50+64, 128)}.Op())
|
||||||
|
opc3.Pop()
|
||||||
|
}, func(r result) {
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// lerp calculates linear interpolation with color b and p.
|
// lerp calculates linear interpolation with color b and p.
|
||||||
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
|
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
|
||||||
return f32color.RGBA{
|
return f32color.RGBA{
|
||||||
|
|||||||
+16
-12
@@ -58,7 +58,7 @@ type coverUniforms struct {
|
|||||||
uvCoverTransform [4]float32
|
uvCoverTransform [4]float32
|
||||||
uvTransformR1 [4]float32
|
uvTransformR1 [4]float32
|
||||||
uvTransformR2 [4]float32
|
uvTransformR2 [4]float32
|
||||||
_ float32
|
fbo float32
|
||||||
}
|
}
|
||||||
|
|
||||||
type stenciler struct {
|
type stenciler struct {
|
||||||
@@ -90,10 +90,10 @@ type intersectUniforms struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fboSet struct {
|
type fboSet struct {
|
||||||
fbos []stencilFBO
|
fbos []FBO
|
||||||
}
|
}
|
||||||
|
|
||||||
type stencilFBO struct {
|
type FBO struct {
|
||||||
size image.Point
|
size image.Point
|
||||||
tex driver.Texture
|
tex driver.Texture
|
||||||
}
|
}
|
||||||
@@ -247,10 +247,10 @@ func newStenciler(ctx driver.Device) *stenciler {
|
|||||||
return st
|
return st
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
|
func (s *fboSet) resize(ctx driver.Device, format driver.TextureFormat, sizes []image.Point) {
|
||||||
// Add fbos.
|
// Add fbos.
|
||||||
for i := len(s.fbos); i < len(sizes); i++ {
|
for i := len(s.fbos); i < len(sizes); i++ {
|
||||||
s.fbos = append(s.fbos, stencilFBO{})
|
s.fbos = append(s.fbos, FBO{})
|
||||||
}
|
}
|
||||||
// Resize fbos.
|
// Resize fbos.
|
||||||
for i, sz := range sizes {
|
for i, sz := range sizes {
|
||||||
@@ -273,7 +273,7 @@ func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
|
|||||||
if sz.X > max {
|
if sz.X > max {
|
||||||
sz.X = max
|
sz.X = max
|
||||||
}
|
}
|
||||||
tex, err := ctx.NewTexture(driver.TextureFormatFloat, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest,
|
tex, err := ctx.NewTexture(format, sz.X, sz.Y, driver.FilterNearest, driver.FilterNearest,
|
||||||
driver.BufferBindingTexture|driver.BufferBindingFramebuffer)
|
driver.BufferBindingTexture|driver.BufferBindingFramebuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -340,15 +340,15 @@ func (s *stenciler) beginIntersect(sizes []image.Point) {
|
|||||||
// 8 bit coverage is enough, but OpenGL ES only supports single channel
|
// 8 bit coverage is enough, but OpenGL ES only supports single channel
|
||||||
// floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
|
// floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
|
||||||
// no floating point support is available.
|
// no floating point support is available.
|
||||||
s.intersections.resize(s.ctx, sizes)
|
s.intersections.resize(s.ctx, driver.TextureFormatFloat, sizes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stenciler) cover(idx int) stencilFBO {
|
func (s *stenciler) cover(idx int) FBO {
|
||||||
return s.fbos.fbos[idx]
|
return s.fbos.fbos[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stenciler) begin(sizes []image.Point) {
|
func (s *stenciler) begin(sizes []image.Point) {
|
||||||
s.fbos.resize(s.ctx, sizes)
|
s.fbos.resize(s.ctx, driver.TextureFormatFloat, sizes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) {
|
func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv image.Point, data pathData) {
|
||||||
@@ -375,11 +375,11 @@ func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv ima
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pather) cover(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
|
func (p *pather) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
|
||||||
p.coverer.cover(mat, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
|
p.coverer.cover(mat, isFBO, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *coverer) cover(mat materialType, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
|
func (c *coverer) cover(mat materialType, isFBO bool, col f32color.RGBA, col1, col2 f32color.RGBA, scale, off f32.Point, uvTrans f32.Affine2D, coverScale, coverOff f32.Point) {
|
||||||
var uniforms *coverUniforms
|
var uniforms *coverUniforms
|
||||||
switch mat {
|
switch mat {
|
||||||
case materialColor:
|
case materialColor:
|
||||||
@@ -399,6 +399,10 @@ func (c *coverer) cover(mat materialType, col f32color.RGBA, col1, col2 f32color
|
|||||||
c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
c.texUniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
|
||||||
uniforms = &c.texUniforms.coverUniforms
|
uniforms = &c.texUniforms.coverUniforms
|
||||||
}
|
}
|
||||||
|
uniforms.fbo = 0
|
||||||
|
if isFBO {
|
||||||
|
uniforms.fbo = 1
|
||||||
|
}
|
||||||
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
uniforms.transform = [4]float32{scale.X, scale.Y, off.X, off.Y}
|
||||||
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
|
uniforms.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
|
||||||
c.pipelines[mat].UploadUniforms(c.ctx)
|
c.pipelines[mat].UploadUniforms(c.ctx)
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func (c *Context) Release() {
|
|||||||
eglDestroyContext(c.disp, c.eglCtx.ctx)
|
eglDestroyContext(c.disp, c.eglCtx.ctx)
|
||||||
c.eglCtx = nil
|
c.eglCtx = nil
|
||||||
}
|
}
|
||||||
|
eglTerminate(c.disp)
|
||||||
c.disp = nilEGLDisplay
|
c.disp = nilEGLDisplay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+48
-5
@@ -19,6 +19,17 @@ type Ops struct {
|
|||||||
data []byte
|
data []byte
|
||||||
// refs hold external references for operations.
|
// refs hold external references for operations.
|
||||||
refs []interface{}
|
refs []interface{}
|
||||||
|
// stringRefs provides space for string references, pointers to which will
|
||||||
|
// be stored in refs. Storing a string directly in refs would cause a heap
|
||||||
|
// allocation, to store the string header in an interface value. The backing
|
||||||
|
// array of stringRefs, on the other hand, gets reused between calls to
|
||||||
|
// reset, making string references free on average.
|
||||||
|
//
|
||||||
|
// Appending to stringRefs might reallocate the backing array, which will
|
||||||
|
// leave pointers to the old array in refs. This temporarily causes a slight
|
||||||
|
// increase in memory usage, but this, too, amortizes away as the capacity
|
||||||
|
// of stringRefs approaches its stable maximum.
|
||||||
|
stringRefs []string
|
||||||
// nextStateID is the id allocated for the next
|
// nextStateID is the id allocated for the next
|
||||||
// StateOp.
|
// StateOp.
|
||||||
nextStateID int
|
nextStateID int
|
||||||
@@ -40,9 +51,10 @@ const (
|
|||||||
TypeMacro OpType = iota + firstOpIndex
|
TypeMacro OpType = iota + firstOpIndex
|
||||||
TypeCall
|
TypeCall
|
||||||
TypeDefer
|
TypeDefer
|
||||||
TypePushTransform
|
|
||||||
TypeTransform
|
TypeTransform
|
||||||
TypePopTransform
|
TypePopTransform
|
||||||
|
TypePushOpacity
|
||||||
|
TypePopOpacity
|
||||||
TypeInvalidate
|
TypeInvalidate
|
||||||
TypeImage
|
TypeImage
|
||||||
TypePaint
|
TypePaint
|
||||||
@@ -111,6 +123,7 @@ const (
|
|||||||
ClipStack StackKind = iota
|
ClipStack StackKind = iota
|
||||||
TransStack
|
TransStack
|
||||||
PassStack
|
PassStack
|
||||||
|
OpacityStack
|
||||||
_StackKind
|
_StackKind
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -124,9 +137,10 @@ const (
|
|||||||
TypeMacroLen = 1 + 4 + 4
|
TypeMacroLen = 1 + 4 + 4
|
||||||
TypeCallLen = 1 + 4 + 4 + 4 + 4
|
TypeCallLen = 1 + 4 + 4 + 4 + 4
|
||||||
TypeDeferLen = 1
|
TypeDeferLen = 1
|
||||||
TypePushTransformLen = 1 + 4*6
|
|
||||||
TypeTransformLen = 1 + 1 + 4*6
|
TypeTransformLen = 1 + 1 + 4*6
|
||||||
TypePopTransformLen = 1
|
TypePopTransformLen = 1
|
||||||
|
TypePushOpacityLen = 1 + 4
|
||||||
|
TypePopOpacityLen = 1
|
||||||
TypeRedrawLen = 1 + 8
|
TypeRedrawLen = 1 + 8
|
||||||
TypeImageLen = 1
|
TypeImageLen = 1
|
||||||
TypePaintLen = 1
|
TypePaintLen = 1
|
||||||
@@ -183,8 +197,12 @@ func Reset(o *Ops) {
|
|||||||
for i := range o.refs {
|
for i := range o.refs {
|
||||||
o.refs[i] = nil
|
o.refs[i] = nil
|
||||||
}
|
}
|
||||||
|
for i := range o.stringRefs {
|
||||||
|
o.stringRefs[i] = ""
|
||||||
|
}
|
||||||
o.data = o.data[:0]
|
o.data = o.data[:0]
|
||||||
o.refs = o.refs[:0]
|
o.refs = o.refs[:0]
|
||||||
|
o.stringRefs = o.stringRefs[:0]
|
||||||
o.nextStateID = 0
|
o.nextStateID = 0
|
||||||
o.version++
|
o.version++
|
||||||
}
|
}
|
||||||
@@ -265,12 +283,26 @@ func Write1(o *Ops, n int, ref1 interface{}) []byte {
|
|||||||
return o.data[len(o.data)-n:]
|
return o.data[len(o.data)-n:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Write1String(o *Ops, n int, ref1 string) []byte {
|
||||||
|
o.data = append(o.data, make([]byte, n)...)
|
||||||
|
o.stringRefs = append(o.stringRefs, ref1)
|
||||||
|
o.refs = append(o.refs, &o.stringRefs[len(o.stringRefs)-1])
|
||||||
|
return o.data[len(o.data)-n:]
|
||||||
|
}
|
||||||
|
|
||||||
func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte {
|
func Write2(o *Ops, n int, ref1, ref2 interface{}) []byte {
|
||||||
o.data = append(o.data, make([]byte, n)...)
|
o.data = append(o.data, make([]byte, n)...)
|
||||||
o.refs = append(o.refs, ref1, ref2)
|
o.refs = append(o.refs, ref1, ref2)
|
||||||
return o.data[len(o.data)-n:]
|
return o.data[len(o.data)-n:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Write2String(o *Ops, n int, ref1 interface{}, ref2 string) []byte {
|
||||||
|
o.data = append(o.data, make([]byte, n)...)
|
||||||
|
o.stringRefs = append(o.stringRefs, ref2)
|
||||||
|
o.refs = append(o.refs, ref1, &o.stringRefs[len(o.stringRefs)-1])
|
||||||
|
return o.data[len(o.data)-n:]
|
||||||
|
}
|
||||||
|
|
||||||
func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
|
func Write3(o *Ops, n int, ref1, ref2, ref3 interface{}) []byte {
|
||||||
o.data = append(o.data, make([]byte, n)...)
|
o.data = append(o.data, make([]byte, n)...)
|
||||||
o.refs = append(o.refs, ref1, ref2, ref3)
|
o.refs = append(o.refs, ref1, ref2, ref3)
|
||||||
@@ -354,6 +386,14 @@ func DecodeTransform(data []byte) (t f32.Affine2D, push bool) {
|
|||||||
return f32.NewAffine2D(a, b, c, d, e, f), push
|
return f32.NewAffine2D(a, b, c, d, e, f), push
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DecodeOpacity(data []byte) float32 {
|
||||||
|
if OpType(data[0]) != TypePushOpacity {
|
||||||
|
panic("invalid op")
|
||||||
|
}
|
||||||
|
bo := binary.LittleEndian
|
||||||
|
return math.Float32frombits(bo.Uint32(data[1:]))
|
||||||
|
}
|
||||||
|
|
||||||
// DecodeSave decodes the state id of a save op.
|
// DecodeSave decodes the state id of a save op.
|
||||||
func DecodeSave(data []byte) int {
|
func DecodeSave(data []byte) int {
|
||||||
if OpType(data[0]) != TypeSave {
|
if OpType(data[0]) != TypeSave {
|
||||||
@@ -381,9 +421,10 @@ var opProps = [0x100]opProp{
|
|||||||
TypeMacro: {Size: TypeMacroLen, NumRefs: 0},
|
TypeMacro: {Size: TypeMacroLen, NumRefs: 0},
|
||||||
TypeCall: {Size: TypeCallLen, NumRefs: 1},
|
TypeCall: {Size: TypeCallLen, NumRefs: 1},
|
||||||
TypeDefer: {Size: TypeDeferLen, NumRefs: 0},
|
TypeDefer: {Size: TypeDeferLen, NumRefs: 0},
|
||||||
TypePushTransform: {Size: TypePushTransformLen, NumRefs: 0},
|
|
||||||
TypeTransform: {Size: TypeTransformLen, NumRefs: 0},
|
TypeTransform: {Size: TypeTransformLen, NumRefs: 0},
|
||||||
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
|
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
|
||||||
|
TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
|
||||||
|
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
|
||||||
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
|
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
|
||||||
TypeImage: {Size: TypeImageLen, NumRefs: 2},
|
TypeImage: {Size: TypeImageLen, NumRefs: 2},
|
||||||
TypePaint: {Size: TypePaintLen, NumRefs: 0},
|
TypePaint: {Size: TypePaintLen, NumRefs: 0},
|
||||||
@@ -440,12 +481,14 @@ func (t OpType) String() string {
|
|||||||
return "Call"
|
return "Call"
|
||||||
case TypeDefer:
|
case TypeDefer:
|
||||||
return "Defer"
|
return "Defer"
|
||||||
case TypePushTransform:
|
|
||||||
return "PushTransform"
|
|
||||||
case TypeTransform:
|
case TypeTransform:
|
||||||
return "Transform"
|
return "Transform"
|
||||||
case TypePopTransform:
|
case TypePopTransform:
|
||||||
return "PopTransform"
|
return "PopTransform"
|
||||||
|
case TypePushOpacity:
|
||||||
|
return "PushOpacity"
|
||||||
|
case TypePopOpacity:
|
||||||
|
return "PopOpacity"
|
||||||
case TypeInvalidate:
|
case TypeInvalidate:
|
||||||
return "Invalidate"
|
return "Invalidate"
|
||||||
case TypeImage:
|
case TypeImage:
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (h ReadOp) Add(o *op.Ops) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h WriteOp) Add(o *op.Ops) {
|
func (h WriteOp) Add(o *op.Ops) {
|
||||||
data := ops.Write1(&o.Internal, ops.TypeClipboardWriteLen, &h.Text)
|
data := ops.Write1String(&o.Internal, ops.TypeClipboardWriteLen, h.Text)
|
||||||
data[0] = byte(ops.TypeClipboardWrite)
|
data[0] = byte(ops.TypeClipboardWrite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -323,8 +323,7 @@ func (h InputOp) Add(o *op.Ops) {
|
|||||||
if h.Tag == nil {
|
if h.Tag == nil {
|
||||||
panic("Tag must be non-nil")
|
panic("Tag must be non-nil")
|
||||||
}
|
}
|
||||||
filter := h.Keys
|
data := ops.Write2String(&o.Internal, ops.TypeKeyInputLen, h.Tag, string(h.Keys))
|
||||||
data := ops.Write2(&o.Internal, ops.TypeKeyInputLen, h.Tag, &filter)
|
|
||||||
data[0] = byte(ops.TypeKeyInput)
|
data[0] = byte(ops.TypeKeyInput)
|
||||||
data[1] = byte(h.Hint)
|
data[1] = byte(h.Hint)
|
||||||
}
|
}
|
||||||
@@ -343,7 +342,7 @@ func (h FocusOp) Add(o *op.Ops) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s SnippetOp) Add(o *op.Ops) {
|
func (s SnippetOp) Add(o *op.Ops) {
|
||||||
data := ops.Write2(&o.Internal, ops.TypeSnippetLen, s.Tag, &s.Text)
|
data := ops.Write2String(&o.Internal, ops.TypeSnippetLen, s.Tag, s.Text)
|
||||||
data[0] = byte(ops.TypeSnippet)
|
data[0] = byte(ops.TypeSnippet)
|
||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
bo.PutUint32(data[1:], uint32(s.Range.Start))
|
bo.PutUint32(data[1:], uint32(s.Range.Start))
|
||||||
|
|||||||
+37
-23
@@ -432,39 +432,42 @@ func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
|
|||||||
return id.id
|
return id.id
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) ActionAt(pos f32.Point) (system.Action, bool) {
|
func (q *pointerQueue) ActionAt(pos f32.Point) (action system.Action, hasAction bool) {
|
||||||
for i := len(q.hitTree) - 1; i >= 0; i-- {
|
q.hitTest(pos, func(n *hitNode) bool {
|
||||||
n := &q.hitTree[i]
|
|
||||||
hit, _ := q.hit(n.area, pos)
|
|
||||||
if !hit {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
area := q.areas[n.area]
|
area := q.areas[n.area]
|
||||||
return area.action, area.action != 0
|
if area.action != 0 {
|
||||||
}
|
action = area.action
|
||||||
return 0, false
|
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()
|
q.assignSemIDs()
|
||||||
for i := len(q.hitTree) - 1; i >= 0; i-- {
|
q.hitTest(pos, func(n *hitNode) bool {
|
||||||
n := &q.hitTree[i]
|
|
||||||
hit, _ := q.hit(n.area, pos)
|
|
||||||
if !hit {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
area := q.areas[n.area]
|
area := q.areas[n.area]
|
||||||
if area.semantic.id != 0 {
|
if area.semantic.id != 0 {
|
||||||
return area.semantic.id, true
|
semID = area.semantic.id
|
||||||
|
hasSemID = true
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
return 0, false
|
})
|
||||||
|
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.
|
// Track whether we're passing through hits.
|
||||||
pass := true
|
pass := true
|
||||||
hits := q.scratch[:0]
|
|
||||||
idx := len(q.hitTree) - 1
|
idx := len(q.hitTree) - 1
|
||||||
cursor := pointer.CursorDefault
|
cursor := pointer.CursorDefault
|
||||||
for idx >= 0 {
|
for idx >= 0 {
|
||||||
@@ -483,12 +486,23 @@ func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
|
|||||||
} else {
|
} else {
|
||||||
idx = n.next
|
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 n.tag != nil {
|
||||||
if _, exists := q.handlers[n.tag]; exists {
|
if _, exists := q.handlers[n.tag]; exists {
|
||||||
hits = addHandler(hits, n.tag)
|
hits = addHandler(hits, n.tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return true
|
||||||
|
})
|
||||||
q.scratch = hits[:0]
|
q.scratch = hits[:0]
|
||||||
return hits, cursor
|
return hits, cursor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
"gioui.org/io/pointer"
|
"gioui.org/io/pointer"
|
||||||
|
"gioui.org/io/system"
|
||||||
"gioui.org/io/transfer"
|
"gioui.org/io/transfer"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
@@ -221,6 +222,43 @@ func TestPointerTypes(t *testing.T) {
|
|||||||
assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, pointer.Release)
|
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) {
|
func TestPointerPriority(t *testing.T) {
|
||||||
handler1 := new(int)
|
handler1 := new(int)
|
||||||
handler2 := 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) {
|
func BenchmarkRouterAdd(b *testing.B) {
|
||||||
// Set this to the number of overlapping handlers that you want to
|
// Set this to the number of overlapping handlers that you want to
|
||||||
// evaluate performance for. Typical values for the example applications
|
// evaluate performance for. Typical values for the example applications
|
||||||
|
|||||||
+4
-4
@@ -490,11 +490,11 @@ func (q *Router) collect() {
|
|||||||
}
|
}
|
||||||
kc.softKeyboard(op.Show)
|
kc.softKeyboard(op.Show)
|
||||||
case ops.TypeKeyInput:
|
case ops.TypeKeyInput:
|
||||||
filter := encOp.Refs[1].(*key.Set)
|
filter := key.Set(*encOp.Refs[1].(*string))
|
||||||
op := key.InputOp{
|
op := key.InputOp{
|
||||||
Tag: encOp.Refs[0].(event.Tag),
|
Tag: encOp.Refs[0].(event.Tag),
|
||||||
Hint: key.InputHint(encOp.Data[1]),
|
Hint: key.InputHint(encOp.Data[1]),
|
||||||
Keys: *filter,
|
Keys: filter,
|
||||||
}
|
}
|
||||||
a := pc.currentArea()
|
a := pc.currentArea()
|
||||||
b := pc.currentAreaBounds()
|
b := pc.currentAreaBounds()
|
||||||
@@ -532,10 +532,10 @@ func (q *Router) collect() {
|
|||||||
|
|
||||||
// Semantic ops.
|
// Semantic ops.
|
||||||
case ops.TypeSemanticLabel:
|
case ops.TypeSemanticLabel:
|
||||||
lbl := encOp.Refs[0].(string)
|
lbl := *encOp.Refs[0].(*string)
|
||||||
pc.semanticLabel(lbl)
|
pc.semanticLabel(lbl)
|
||||||
case ops.TypeSemanticDesc:
|
case ops.TypeSemanticDesc:
|
||||||
desc := encOp.Refs[0].(string)
|
desc := *encOp.Refs[0].(*string)
|
||||||
pc.semanticDesc(desc)
|
pc.semanticDesc(desc)
|
||||||
case ops.TypeSemanticClass:
|
case ops.TypeSemanticClass:
|
||||||
class := semantic.ClassOp(encOp.Data[1])
|
class := semantic.ClassOp(encOp.Data[1])
|
||||||
|
|||||||
@@ -40,12 +40,12 @@ type SelectedOp bool
|
|||||||
type DisabledOp bool
|
type DisabledOp bool
|
||||||
|
|
||||||
func (l LabelOp) Add(o *op.Ops) {
|
func (l LabelOp) Add(o *op.Ops) {
|
||||||
data := ops.Write1(&o.Internal, ops.TypeSemanticLabelLen, string(l))
|
data := ops.Write1String(&o.Internal, ops.TypeSemanticLabelLen, string(l))
|
||||||
data[0] = byte(ops.TypeSemanticLabel)
|
data[0] = byte(ops.TypeSemanticLabel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DescriptionOp) Add(o *op.Ops) {
|
func (d DescriptionOp) Add(o *op.Ops) {
|
||||||
data := ops.Write1(&o.Internal, ops.TypeSemanticDescLen, string(d))
|
data := ops.Write1String(&o.Internal, ops.TypeSemanticDescLen, string(d))
|
||||||
data[0] = byte(ops.TypeSemanticDesc)
|
data[0] = byte(ops.TypeSemanticDesc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,9 @@ func (p *Path) Line(delta f32.Point) {
|
|||||||
|
|
||||||
// LineTo moves the pen to the absolute point specified, recording a line.
|
// LineTo moves the pen to the absolute point specified, recording a line.
|
||||||
func (p *Path) LineTo(to f32.Point) {
|
func (p *Path) LineTo(to f32.Point) {
|
||||||
|
if to == p.pen {
|
||||||
|
return
|
||||||
|
}
|
||||||
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
|
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
|
||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
bo.PutUint32(data[0:], uint32(p.contour))
|
bo.PutUint32(data[0:], uint32(p.contour))
|
||||||
@@ -250,6 +253,9 @@ func (p *Path) Quad(ctrl, to f32.Point) {
|
|||||||
// QuadTo records a quadratic Bézier from the pen to end
|
// QuadTo records a quadratic Bézier from the pen to end
|
||||||
// with the control point ctrl, with absolute coordinates.
|
// with the control point ctrl, with absolute coordinates.
|
||||||
func (p *Path) QuadTo(ctrl, to f32.Point) {
|
func (p *Path) QuadTo(ctrl, to f32.Point) {
|
||||||
|
if ctrl == p.pen && to == p.pen {
|
||||||
|
return
|
||||||
|
}
|
||||||
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
|
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
|
||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
bo.PutUint32(data[0:], uint32(p.contour))
|
bo.PutUint32(data[0:], uint32(p.contour))
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ type LinearGradientOp struct {
|
|||||||
type PaintOp struct {
|
type PaintOp struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OpacityStack represents an opacity applied to all painting operations
|
||||||
|
// until Pop is called.
|
||||||
|
type OpacityStack struct {
|
||||||
|
id ops.StackID
|
||||||
|
macroID int
|
||||||
|
ops *ops.Ops
|
||||||
|
}
|
||||||
|
|
||||||
// NewImageOp creates an ImageOp backed by src.
|
// NewImageOp creates an ImageOp backed by src.
|
||||||
//
|
//
|
||||||
// NewImageOp assumes the backing image is immutable, and may cache a
|
// NewImageOp assumes the backing image is immutable, and may cache a
|
||||||
@@ -145,3 +153,31 @@ func Fill(ops *op.Ops, c color.NRGBA) {
|
|||||||
ColorOp{Color: c}.Add(ops)
|
ColorOp{Color: c}.Add(ops)
|
||||||
PaintOp{}.Add(ops)
|
PaintOp{}.Add(ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PushOpacity creates a drawing layer with an opacity in the range [0;1].
|
||||||
|
// The layer includes every subsequent drawing operation until [OpacityStack.Pop]
|
||||||
|
// is called.
|
||||||
|
//
|
||||||
|
// The layer is drawn in two steps. First, the layer operations are
|
||||||
|
// drawn to a separate image. Then, the image is blended on top of
|
||||||
|
// the frame, with the opacity used as the blending factor.
|
||||||
|
func PushOpacity(o *op.Ops, opacity float32) OpacityStack {
|
||||||
|
if opacity > 1 {
|
||||||
|
opacity = 1
|
||||||
|
}
|
||||||
|
if opacity < 0 {
|
||||||
|
opacity = 0
|
||||||
|
}
|
||||||
|
id, macroID := ops.PushOp(&o.Internal, ops.OpacityStack)
|
||||||
|
data := ops.Write(&o.Internal, ops.TypePushOpacityLen)
|
||||||
|
bo := binary.LittleEndian
|
||||||
|
data[0] = byte(ops.TypePushOpacity)
|
||||||
|
bo.PutUint32(data[1:], math.Float32bits(opacity))
|
||||||
|
return OpacityStack{ops: &o.Internal, id: id, macroID: macroID}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t OpacityStack) Pop() {
|
||||||
|
ops.PopOp(t.ops, ops.OpacityStack, t.id, t.macroID)
|
||||||
|
data := ops.Write(t.ops, ops.TypePopOpacityLen)
|
||||||
|
data[0] = byte(ops.TypePopOpacity)
|
||||||
|
}
|
||||||
|
|||||||
+67
-67
@@ -36,7 +36,8 @@ type document struct {
|
|||||||
lines []line
|
lines []line
|
||||||
alignment Alignment
|
alignment Alignment
|
||||||
// alignWidth is the width used when aligning text.
|
// 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
|
// 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.lines = l.lines[:0]
|
||||||
l.alignment = Start
|
l.alignment = Start
|
||||||
l.alignWidth = 0
|
l.alignWidth = 0
|
||||||
|
l.unreadRuneCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func max(a, b int) int {
|
func max(a, b int) int {
|
||||||
@@ -93,6 +95,55 @@ type line struct {
|
|||||||
yOffset int
|
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
|
// 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
|
// within a larger slice. The unit is usually runes of unicode data or
|
||||||
// glyphs of shaped font data.
|
// glyphs of shaped font data.
|
||||||
@@ -528,35 +579,21 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
|||||||
if hasNewline {
|
if hasNewline {
|
||||||
txt = txt[:len(txt)-1]
|
txt = txt[:len(txt)-1]
|
||||||
}
|
}
|
||||||
truncatedNewline := false
|
if params.MaxLines != 0 && hasNewline {
|
||||||
if hasNewline && len(txt) == 0 {
|
// If we might end up truncating a trailing newline, we must insert the truncator symbol
|
||||||
params.forceTruncate = false
|
// on the final line (if we hit the limit).
|
||||||
// If we only have a newline, shape a space to get line metrics.
|
params.forceTruncate = true
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
|
||||||
|
|
||||||
didTruncate := truncated > 0 || truncatedNewline || (params.forceTruncate && params.MaxLines == len(ls))
|
hasTruncator := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
|
||||||
|
if hasTruncator && hasNewline {
|
||||||
if didTruncate && hasNewline {
|
// We have a truncator at the end of the line, so the newline is logically
|
||||||
// We've truncated the newline, since it was at the end and we've truncated some amount of runes
|
// truncated as well.
|
||||||
// before it.
|
|
||||||
truncated++
|
truncated++
|
||||||
hasNewline = false
|
hasNewline = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to Lines.
|
// Convert to Lines.
|
||||||
textLines := make([]line, len(ls))
|
textLines := make([]line, len(ls))
|
||||||
maxHeight := fixed.Int26_6(0)
|
maxHeight := fixed.Int26_6(0)
|
||||||
@@ -565,49 +602,12 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
|
|||||||
if otLine.lineHeight > maxHeight {
|
if otLine.lineHeight > maxHeight {
|
||||||
maxHeight = otLine.lineHeight
|
maxHeight = otLine.lineHeight
|
||||||
}
|
}
|
||||||
isFinalLine := i == len(ls)-1
|
if isFinalLine := i == len(ls)-1; isFinalLine {
|
||||||
if isFinalLine && hasNewline {
|
if hasNewline {
|
||||||
// If there was a trailing newline update the rune counts to include
|
otLine.insertTrailingSyntheticNewline(len(txt))
|
||||||
// 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,
|
|
||||||
}
|
}
|
||||||
// Inset the synthetic newline glyph on the proper end of the run.
|
if hasTruncator {
|
||||||
if otLine.runs[finalRunIdx].Direction.Progression() == system.FromOrigin {
|
otLine.setTruncatedCount(truncated)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
textLines[i] = otLine
|
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
|
// NewShaper constructs a shaper with the provided options.
|
||||||
// available.
|
//
|
||||||
|
// 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 {
|
func NewShaper(options ...ShaperOption) *Shaper {
|
||||||
l := &Shaper{}
|
l := &Shaper{}
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
@@ -351,11 +354,7 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
|
|||||||
unreadRunes++
|
unreadRunes++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastLineIdx := len(lines.lines) - 1
|
l.txt.unreadRuneCount = unreadRunes
|
||||||
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.append(lines)
|
l.txt.append(lines)
|
||||||
@@ -505,6 +504,9 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
|
|||||||
}
|
}
|
||||||
if endOfCluster {
|
if endOfCluster {
|
||||||
glyph.Flags |= FlagClusterBreak
|
glyph.Flags |= FlagClusterBreak
|
||||||
|
if run.truncator {
|
||||||
|
glyph.Runes += l.txt.unreadRuneCount
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
glyph.Runes = 0
|
glyph.Runes = 0
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-16
@@ -154,9 +154,11 @@ func TestWrappingForcedTruncation(t *testing.T) {
|
|||||||
// consistently and does not create spurious lines of text.
|
// consistently and does not create spurious lines of text.
|
||||||
func TestShapingNewlineHandling(t *testing.T) {
|
func TestShapingNewlineHandling(t *testing.T) {
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
textInput string
|
textInput string
|
||||||
expectedLines int
|
expectedLines int
|
||||||
expectedGlyphs int
|
expectedGlyphs int
|
||||||
|
maxLines int
|
||||||
|
expectedTruncated int
|
||||||
}
|
}
|
||||||
for _, tc := range []testcase{
|
for _, tc := range []testcase{
|
||||||
{textInput: "a\n", expectedLines: 1, expectedGlyphs: 3},
|
{textInput: "a\n", expectedLines: 1, expectedGlyphs: 3},
|
||||||
@@ -165,21 +167,41 @@ func TestShapingNewlineHandling(t *testing.T) {
|
|||||||
{textInput: "\n", expectedLines: 1, expectedGlyphs: 2},
|
{textInput: "\n", expectedLines: 1, expectedGlyphs: 2},
|
||||||
{textInput: "\n\n", expectedLines: 2, expectedGlyphs: 3},
|
{textInput: "\n\n", expectedLines: 2, expectedGlyphs: 3},
|
||||||
{textInput: "\n\n\n", expectedLines: 3, expectedGlyphs: 4},
|
{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)
|
ltrFace, _ := opentype.Parse(goregular.TTF)
|
||||||
collection := []FontFace{{Face: ltrFace}}
|
collection := []FontFace{{Face: ltrFace}}
|
||||||
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
|
||||||
checkGlyphs := func() {
|
checkGlyphs := func() {
|
||||||
glyphs := []Glyph{}
|
glyphs := []Glyph{}
|
||||||
runes := 0
|
runes := 0
|
||||||
|
truncated := 0
|
||||||
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
|
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
|
||||||
glyphs = append(glyphs, g)
|
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)
|
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 {
|
if len(glyphs) != tc.expectedGlyphs {
|
||||||
t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs))
|
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")
|
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)
|
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,
|
Alignment: Middle,
|
||||||
PxPerEm: fixed.I(10),
|
PxPerEm: fixed.I(10),
|
||||||
MinWidth: 200,
|
MinWidth: 200,
|
||||||
MaxWidth: 200,
|
MaxWidth: 200,
|
||||||
Locale: english,
|
Locale: english,
|
||||||
}, tc.textInput)
|
MaxLines: tc.maxLines,
|
||||||
|
}
|
||||||
|
cache.LayoutString(params, tc.textInput)
|
||||||
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
||||||
t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount)
|
t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount)
|
||||||
}
|
}
|
||||||
checkGlyphs()
|
checkGlyphs()
|
||||||
|
|
||||||
cache.Layout(Parameters{
|
cache.Layout(params, strings.NewReader(tc.textInput))
|
||||||
Alignment: Middle,
|
|
||||||
PxPerEm: fixed.I(10),
|
|
||||||
MinWidth: 200,
|
|
||||||
MaxWidth: 200,
|
|
||||||
Locale: english,
|
|
||||||
}, strings.NewReader(tc.textInput))
|
|
||||||
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
|
||||||
t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount)
|
t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount)
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-5
@@ -28,6 +28,7 @@ type Clickable struct {
|
|||||||
keyTag struct{}
|
keyTag struct{}
|
||||||
requestFocus bool
|
requestFocus bool
|
||||||
focused bool
|
focused bool
|
||||||
|
pressedKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Click represents a click.
|
// Click represents a click.
|
||||||
@@ -178,17 +179,30 @@ func (b *Clickable) update(gtx layout.Context) {
|
|||||||
switch e := e.(type) {
|
switch e := e.(type) {
|
||||||
case key.FocusEvent:
|
case key.FocusEvent:
|
||||||
b.focused = e.Focus
|
b.focused = e.Focus
|
||||||
|
if !b.focused {
|
||||||
|
b.pressedKey = ""
|
||||||
|
}
|
||||||
case key.Event:
|
case key.Event:
|
||||||
if !b.focused || e.State != key.Release {
|
if !b.focused {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if e.Name != key.NameReturn && e.Name != key.NameSpace {
|
if e.Name != key.NameReturn && e.Name != key.NameSpace {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
b.clicks = append(b.clicks, Click{
|
switch e.State {
|
||||||
Modifiers: e.Modifiers,
|
case key.Press:
|
||||||
NumClicks: 1,
|
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