Compare commits

...

24 Commits

Author SHA1 Message Date
Elias Naur b66dcc436c app: [macOS] fix transition from maximized to restored
The NSWindow.zoomed property is not reliable when a window is being
constructed. Only call it when necessary.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-02 18:48:18 -05:00
Elias Naur 526db27c75 gpu: fix opacity layer rendering on OpenGL
Fixes: https://todo.sr.ht/~eliasnaur/gio/536
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-10-02 13:07:15 -05:00
Chris Waldon 27193ae8e8 op/clip: prevent no-op path segments
This commit prevents the insertion of LineTo and QuadTo path segments that have
no visible effect on the path (because the path's pen is already at their end state).
This eliminates whisker artifacts from some stroked paths. Thanks to Morlay for the
bug report leading to this fix.

Fixes: https://todo.sr.ht/~eliasnaur/gio/535
Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-09-18 09:28:11 -05:00
Elias Naur 313c488ec3 app: [Windows] remove padding from maximized custom decorated windows
As described in https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543
Windows extends maximized windows outside the visible display. This is
not appropriate for custom decorated windows, so this change implements
a workaround in the handling of WM_NCCALCSIZE.

While here, replace the deltas field from window state to fix issues
when switching between decoration modes.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-09-08 15:22:40 -05:00
Elias Naur f30e936d9a app: [Windows] remove redundant call to SetWindowText
And fix a typo while here.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-09-08 15:12:47 -05:00
Elias Naur ae3bd2a1e1 op/paint: add opacity operation
The new paint.PushOpacity allows for adjusting the opacity of a group
of drawing operations.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-09-08 11:46:17 -05:00
Elias Naur ae43d18ced internal/ops: remove unused TypePushTransform
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-09-07 14:48:11 -05:00
Dominik Honnef b4d93379c4 op: don't allocate for each string reference
When storing a string in an interface value that escapes, Go has to heap
allocate space for the string header, as interface values can only store
pointers. In text-heavy applications, this can lead to hundreds of
allocations per frame due to semantic.LabelOp, the primary user of
string-typed references in ops.

Instead of allocating each string header individually, provide a slice
of strings to store string-typed references in, and store pointers into
this slice as the actual references. This only allocates when resizing
the slice's backing array, and averages out to no allocations, as the
backing array gets reused between calls to Ops.Reset.

We introduce two new functions, Write1String and Write2String, which
make use of this new slice for their last argument. We could've
automated this in the existing Write1 and Write2 methods, but that would
require type assertions on each call, and the vast majority of ops do
not make use of strings.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-09-02 09:02:39 -06:00
Dominik Honnef b9654eb4eb app: support numpad keys in xkb
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-09-01 16:34:16 -06:00
Dominik Honnef 89d20c7d99 app: [macOS] handle mouse dragging with buttons other than the left one
Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-08-31 15:52:44 -04:00
Dominik Honnef 14bab8efae app: [macOS] handle middle mouse button correctly
NSView only has events for left, right, and other. Also, the Go side
wasn't actually checking for buttons other than left and right.

Signed-off-by: Dominik Honnef <dominik@honnef.co>
2023-08-31 15:52:39 -04:00
Chris Waldon f437aaf359 io/router: fix semantic area traversal
This commit updates the logic behind SemanticAt to use the same hit area
traversal as normal event routing, which should result in more accurate
results for screen readers trying to resolve widgets that might be partially
obscured by non-semantic content.

While here, I realized that the iteration of hit areas needed to stop at
the first matching semantic area, and I added that capability and updated
the ActionAt logic to leverage it as well.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-31 15:09:05 -04:00
Elias Naur cf5ae4aad9 internal/egl: call eglTerminate after context release
Without eglTerminate, using EGL will crash or report spurious errors after
creating and destroying enough contexts. The test program in #528
takes 5-10 window cycles before errors show up for me.

Fixes: https://todo.sr.ht/~eliasnaur/gio/528
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-23 13:31:31 -06:00
Chris Waldon 8679f49fff text: [API] be absolutely consistent about newline truncation
This commit changes the shaper's behavior when truncating text. Previously, if the final
line allowed by MaxLines ended with a newline, whether or not that newline was truncated
depended upon whether we knew that there was more text after the current paragraph. However,
this makes reasoning about what the shaper will do quite difficult. It seems better to be
consistent. Now we will insert a truncator at the end of the final line if it has a trailing
newline character, regardless of whether it ends the input text.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-22 16:37:24 -06:00
Chris Waldon 83202263b9 text: simplify truncation accounting
This commit reverts the work of several previous attempts to resolve truncation-related
rune accounting problems and adopts a simpler approach. Instead of taking a special codepath
when shaping only a newline, we shape the empty string to get its line metrics. Instead of
modifying the final glyph conditionally to account for runes we never actually shaped, we
track that count on the document type and handle it withing the NextGlyph method.

These changes result in much simpler code, and resolve a real bug. We were accidentally corrupting
cached paragraphs when doing the truncation post-processing in Shaper.layoutText. The modification
made to the final glyph there actually did modify the cached copy, which would then be reused when
that string was shaped again (even if there were a different number of truncated runes after it).

This changeset ensures that the cached copy of a paragraph is never modified.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-22 16:37:20 -06:00
Elias Naur 7fde80e805 app: [Wayland] avoid a race on the send side of the wakeup pipe
Discovered while debugging #528 with -race.

References: https://todo.sr.ht/~eliasnaur/gio/528
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-22 16:33:12 -06:00
Elias Naur e9d0619641 app: [macOS] stop display after any events that may access it
Fixes: https://todo.sr.ht/~eliasnaur/gio/527
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-22 11:57:43 -06:00
Elias Naur 2e524200ab app: [macOS] fix display link callback race
Commit c0c25b777 replaced the synchronizing of the display link callback
from a sync.Map to a cgo.Handle. However, the change didn't take into
account the lifecycle issues: a callback may happen just as the cgo.Handle
is freed, leading to a misuse crash.

This change restores the sync.Map synchronization, which avoids the
lifecycle issue.

Fixes: https://todo.sr.ht/~eliasnaur/gio/526
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-22 11:02:49 -06:00
Chris Waldon cc477e9ca6 app: [Windows] ensure custom window decorations allow resize
This commit fixes a platform inconsistency that prevented custom-decorated windows
from being resizable on edges where their custom decorations placed a draggable
system.ActionInputOp.

The prior behavior always checked for this action type before
checking if the cursor was potentially in a window resize area, which meant that
for windows with material.Decorations, it was impossible to resize those windows
from their top edge. The system.ActionMove handler would always win. This is not
the case on platforms like macOS, so this commit makes the behavior consistent by
prioritizing resize over drag.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-22 09:08:06 -06:00
Veikko Sariola 290b5fe821 widget: click button only if key pressed and released
This commit fixes the non-intuitive behaviour, where hitting return or
space with a button focused, then tabbing to another button and
releasing the key causes the second button to trigger. It feels wrong,
as the "gesture" was never initiated on the second button. The fix makes
widget.Clickable track which key was pressed, in a variable called
pressedKey, and only considers a key release if the released key matches
the pressed key. Finally, if the widget loses focus, pressedKey is
cleared.

Fixes: https://todo.sr.ht/~eliasnaur/gio/525
Signed-off-by: Veikko Sariola <5684185+vsariola@users.noreply.github.com>
2023-08-22 09:07:48 -06:00
Chris Waldon e9cb0b326d io/router: fix system action routing logic
When running ActionAt, the router used to only consider the topmost clip area, even
if that clip area had no input handlers attached whatsoever. This change updates the
logic for that test to use the same traversal as normal event handling, ensuring that
action inputs behave intuitively like any other pointer input area. Included is a test
catching the problematic behavior that prompted this change.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-21 10:44:44 -06:00
Elias Naur 0e77a2b521 app: [Windows] enable drop shadows for custom decorated windows
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-16 15:44:04 -06:00
Elias Naur 63550cc81e app: [Windows] make custom decorated windows behave like regular windows
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2023-08-16 15:44:04 -06:00
Chris Waldon 03c21dc1b5 text: add android portability notice to NewShaper
NewShaper cannot be called prior to opening an application window on Android unless
the application does not want system font support. Add a note to this effect to the
constructor.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-08-12 08:16:28 -06:00
29 changed files with 874 additions and 289 deletions
+41
View File
@@ -47,6 +47,13 @@ type WndClassEx struct {
HIconSm syscall.Handle
}
type Margins struct {
CxLeftWidth int32
CxRightWidth int32
CyTopHeight int32
CyBottomHeight int32
}
type Msg struct {
Hwnd syscall.Handle
Message uint32
@@ -69,6 +76,21 @@ type MinMaxInfo struct {
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 {
length uint32
flags uint32
@@ -245,6 +267,7 @@ const (
WM_MOUSEHWHEEL = 0x020E
WM_NCACTIVATE = 0x0086
WM_NCHITTEST = 0x0084
WM_NCCALCSIZE = 0x0083
WM_PAINT = 0x000F
WM_QUIT = 0x0012
WM_SETCURSOR = 0x0020
@@ -323,6 +346,7 @@ var (
_DispatchMessage = user32.NewProc("DispatchMessageW")
_EmptyClipboard = user32.NewProc("EmptyClipboard")
_GetWindowRect = user32.NewProc("GetWindowRect")
_GetClientRect = user32.NewProc("GetClientRect")
_GetClipboardData = user32.NewProc("GetClipboardData")
_GetDC = user32.NewProc("GetDC")
_GetDpiForWindow = user32.NewProc("GetDpiForWindow")
@@ -379,6 +403,9 @@ var (
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
dwmapi = syscall.NewLazySystemDLL("dwmapi")
_DwmExtendFrameIntoClientArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
)
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
@@ -430,6 +457,14 @@ func DispatchMessage(m *Msg) {
_DispatchMessage.Call(uintptr(unsafe.Pointer(m)))
}
func DwmExtendFrameIntoClientArea(hwnd syscall.Handle, margins Margins) error {
r, _, _ := _DwmExtendFrameIntoClientArea.Call(uintptr(hwnd), uintptr(unsafe.Pointer(&margins)))
if r != 0 {
return fmt.Errorf("DwmExtendFrameIntoClientArea: %#x", r)
}
return nil
}
func EmptyClipboard() error {
r, _, err := _EmptyClipboard.Call()
if r == 0 {
@@ -444,6 +479,12 @@ func GetWindowRect(hwnd syscall.Handle) Rect {
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) {
r, _, err := _GetClipboardData.Call(uintptr(format))
if r == 0 {
+63 -4
View File
@@ -242,6 +242,9 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
if 'a' <= s && s <= 'z' {
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 <= '~' {
return string(rune(s)), true
}
@@ -255,8 +258,6 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
n = key.NameRightArrow
case C.XKB_KEY_Return:
n = key.NameReturn
case C.XKB_KEY_KP_Enter:
n = key.NameEnter
case C.XKB_KEY_Up:
n = key.NameUpArrow
case C.XKB_KEY_Down:
@@ -297,9 +298,9 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
n = key.NameF11
case C.XKB_KEY_F12:
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
case 0x20, C.XKB_KEY_KP_Space:
case 0x20:
n = key.NameSpace
case C.XKB_KEY_Control_L, C.XKB_KEY_Control_R:
n = key.NameCtrl
@@ -309,6 +310,64 @@ func convertKeysym(s C.xkb_keysym_t) (string, bool) {
n = key.NameAlt
case C.XKB_KEY_Super_L, C.XKB_KEY_Super_R:
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:
return "", false
}
+18 -11
View File
@@ -6,7 +6,7 @@ package app
#include <Foundation/Foundation.h>
__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(uintptr_t handle);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void);
__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl);
__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
@@ -42,7 +42,7 @@ static CFTypeRef newNSString(unichar *chars, NSUInteger length) {
import "C"
import (
"errors"
"runtime/cgo"
"sync"
"sync/atomic"
"time"
"unicode/utf16"
@@ -70,6 +70,9 @@ type displayLink struct {
running uint32
}
// displayLinks maps CFTypeRefs to *displayLinks.
var displayLinks sync.Map
var mainFuncs = make(chan func(), 1)
// runOnMain runs the function on the main thread.
@@ -128,18 +131,18 @@ func NewDisplayLink(callback func()) (*displayLink, error) {
states: make(chan bool),
dids: make(chan uint64),
}
h := cgo.NewHandle(d)
dl := C.gio_createDisplayLink(C.uintptr_t(h))
dl := C.gio_createDisplayLink()
if dl == 0 {
return nil, errors.New("app: failed to create display link")
}
go d.run(dl, h)
go d.run(dl)
return d, nil
}
func (d *displayLink) run(dl C.CFTypeRef, h cgo.Handle) {
func (d *displayLink) run(dl C.CFTypeRef) {
defer C.gio_releaseDisplayLink(dl)
defer h.Delete()
displayLinks.Store(dl, d)
defer displayLinks.Delete(dl)
var stopTimer *time.Timer
var tchan <-chan time.Time
started := false
@@ -200,10 +203,14 @@ func (d *displayLink) SetDisplayID(did uint64) {
}
//export gio_onFrameCallback
func gio_onFrameCallback(dl C.CFTypeRef, handle C.uintptr_t) {
d := cgo.Handle(handle).Value().(*displayLink)
if atomic.LoadUint32(&d.running) != 0 {
d.callback()
func gio_onFrameCallback(ref C.CFTypeRef) {
d, exists := displayLinks.Load(ref)
if !exists {
return
}
dl := d.(*displayLink)
if atomic.LoadUint32(&dl.running) != 0 {
dl.callback()
}
}
+5 -17
View File
@@ -123,6 +123,9 @@ static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIE
@implementation GioView
NSArray<UIKeyCommand *> *_keyCommands;
+ (void)onFrameCallback:(CADisplayLink *)link {
gio_onFrameCallback((__bridge CFTypeRef)link);
}
+ (Class)layerClass {
return gio_layerClass();
}
@@ -227,23 +230,8 @@ NSArray<UIKeyCommand *> *_keyCommands;
}
@end
@interface DisplayLinkHandle : NSObject {
}
@property uintptr_t handle;
@end
@implementation DisplayLinkHandle {
}
- (void)onFrameCallback:(CADisplayLink *)link {
gio_onFrameCallback((__bridge CFTypeRef)link, _handle);
}
@end
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
DisplayLinkHandle *h = [DisplayLinkHandle alloc];
h.handle = handle;
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:h selector:@selector(onFrameCallback:)];
CFTypeRef gio_createDisplayLink(void) {
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)];
dl.paused = YES;
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
[dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
+8 -6
View File
@@ -365,11 +365,11 @@ func (w *window) Configure(options []Option) {
case Minimized:
C.unhideWindow(window)
case Maximized:
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
}
w.config.Mode = Windowed
if C.isWindowZoomed(window) != 0 {
C.zoomWindow(window)
}
w.setTitle(prev, cnf)
if prev.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
case 1:
btn = pointer.ButtonSecondary
case 2:
btn = pointer.ButtonTertiary
}
var typ pointer.Type
switch cdir {
@@ -788,13 +790,13 @@ func configFor(scale float32) unit.Metric {
//export gio_onClose
func gio_onClose(view C.CFTypeRef) {
w := mustView(view)
w.displayLink.Close()
w.w.Event(ViewEvent{})
deleteView(view)
w.w.Event(system.DestroyEvent{})
w.displayLink.Close()
w.displayLink = nil
deleteView(view)
C.CFRelease(w.view)
w.view = 0
w.displayLink = nil
}
//export gio_onHide
+15 -9
View File
@@ -92,24 +92,30 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
- (void)mouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0);
}
- (void)middleMouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
}
- (void)middleMouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0);
}
- (void)rightMouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
}
- (void)rightMouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0);
}
- (void)otherMouseDown:(NSEvent *)event {
handleMouse(self, event, MOUSE_DOWN, 0, 0);
}
- (void)otherMouseUp:(NSEvent *)event {
handleMouse(self, event, MOUSE_UP, 0, 0);
}
- (void)mouseMoved:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
- (void)mouseDragged:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
- (void)rightMouseDragged:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
- (void)otherMouseDragged:(NSEvent *)event {
handleMouse(self, event, MOUSE_MOVE, 0, 0);
}
- (void)scrollWheel:(NSEvent *)event {
CGFloat dx = -event.scrollingDeltaX;
CGFloat dy = -event.scrollingDeltaY;
@@ -193,14 +199,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
static GioWindowDelegate *globalWindowDel;
static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *handle) {
gio_onFrameCallback(dl, (uintptr_t)handle);
gio_onFrameCallback(dl);
return kCVReturnSuccess;
}
CFTypeRef gio_createDisplayLink(uintptr_t handle) {
CFTypeRef gio_createDisplayLink(void) {
CVDisplayLinkRef dl;
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, (void *)(handle));
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil);
return dl;
}
+11 -1
View File
@@ -94,7 +94,10 @@ type wlDisplay struct {
// Notification pipe fds.
notify struct {
read, write int
read int
mu sync.Mutex
write int
}
repeat repeatState
@@ -1442,6 +1445,11 @@ func (w *window) SetAnimating(anim bool) {
// Wakeup wakes up the event loop through the notification pipe.
func (d *wlDisplay) wakeup() {
oneByte := make([]byte, 1)
d.notify.mu.Lock()
defer d.notify.mu.Unlock()
if d.notify.write == 0 {
return
}
if _, err := syscall.Write(d.notify.write, oneByte); err != nil && err != syscall.EAGAIN {
panic(fmt.Errorf("failed to write to pipe: %v", err))
}
@@ -1820,10 +1828,12 @@ func newWLDisplay() (*wlDisplay, error) {
}
func (d *wlDisplay) destroy() {
d.notify.mu.Lock()
if d.notify.write != 0 {
syscall.Close(d.notify.write)
d.notify.write = 0
}
d.notify.mu.Unlock()
if d.notify.read != 0 {
syscall.Close(d.notify.read)
d.notify.read = 0
+63 -50
View File
@@ -32,11 +32,6 @@ type ViewEvent struct {
HWND uintptr
}
type winDeltas struct {
width int32
height int32
}
type window struct {
hwnd syscall.Handle
hdc syscall.Handle
@@ -55,7 +50,6 @@ type window struct {
animating bool
focused bool
deltas winDeltas
borderSize image.Point
config Config
}
@@ -192,22 +186,12 @@ func createNativeWindow() (*window, error) {
// It reads the window style and size/position and updates w.config.
// If anything has changed it emits a ConfigEvent to notify the application.
func (w *window) update() {
r := windows.GetWindowRect(w.hwnd)
size := image.Point{
X: int(r.Right - r.Left - w.deltas.width),
Y: int(r.Bottom - r.Top - w.deltas.height),
cr := windows.GetClientRect(w.hwnd)
w.config.Size = image.Point{
X: int(cr.Right - cr.Left),
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(
windows.GetSystemMetrics(windows.SM_CXSIZEFRAME),
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.
w.hwnd = 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:
w.draw(true)
case windows.WM_SIZE:
@@ -344,18 +350,26 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
}
case windows.WM_GETMINMAXINFO:
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 {
mm.PtMinTrackSize = windows.Point{
X: int32(p.X) + w.deltas.width,
Y: int32(p.Y) + w.deltas.height,
X: int32(p.X) + bw,
Y: int32(p.Y) + bh,
}
}
if p := w.config.MaxSize; p.X > 0 || p.Y > 0 {
mm.PtMaxTrackSize = windows.Point{
X: int32(p.X) + w.deltas.width,
Y: int32(p.Y) + w.deltas.height,
X: int32(p.X) + bw,
Y: int32(p.Y) + bh,
}
}
return 0
case windows.WM_SETCURSOR:
w.cursorIn = (lParam & 0xffff) == windows.HTCLIENT
if w.cursorIn {
@@ -446,23 +460,18 @@ func (w *window) hitTest(x, y int) uintptr {
if w.config.Mode == Fullscreen {
return windows.HTCLIENT
}
p := f32.Pt(float32(x), float32(y))
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
return windows.HTCAPTION
}
if w.config.Mode != Windowed {
// Only windowed mode should allow resizing.
return windows.HTCLIENT
}
// Check for resize handle before system actions; otherwise it can be impossible to
// resize a custom-decorations window when the system move area is flush with the
// edge of the window.
top := y <= w.borderSize.Y
bottom := y >= w.config.Size.Y-w.borderSize.Y
left := x <= w.borderSize.X
right := x >= w.config.Size.X-w.borderSize.X
switch {
default:
fallthrough
case !top && !bottom && !left && !right:
return windows.HTCLIENT
case top && left:
return windows.HTTOPLEFT
case top && right:
@@ -480,6 +489,11 @@ func (w *window) hitTest(x, y int) uintptr {
case right:
return windows.HTRIGHT
}
p := f32.Pt(float32(x), float32(y))
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
return windows.HTCAPTION
}
return windows.HTCLIENT
}
func (w *window) pointerButton(btn pointer.Buttons, press bool, lParam uintptr, kmods key.Modifiers) {
@@ -669,9 +683,6 @@ func (w *window) Configure(options []Option) {
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
style &^= winStyle
if !w.config.Decorated {
winStyle = 0
}
switch w.config.Mode {
case Minimized:
style |= winStyle
@@ -684,28 +695,30 @@ func (w *window) Configure(options []Option) {
showMode = windows.SW_SHOWMAXIMIZED
case Windowed:
windows.SetWindowText(w.hwnd, w.config.Title)
style |= winStyle
showMode = windows.SW_SHOWNORMAL
// Get target for client areaa size.
// Get target for client area size.
width = int32(w.config.Size.X)
height = int32(w.config.Size.Y)
// Get the current window size and position.
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
y = wr.Top
width = r.Right - r.Left
height = r.Bottom - r.Top
if w.config.Decorated {
// Compute client size and position. Note that the client size is
// equal to the window size when we are in control of decorations.
r := windows.Rect{
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:
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.ShowWindow(w.hwnd, showMode)
w.w.Event(ConfigEvent{Config: w.config})
w.update()
}
func (w *window) WriteClipboard(s string) {
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.19
require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2
gioui.org/shader v1.0.6
gioui.org/shader v1.0.8
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372
golang.org/x/exp v0.0.0-20221012211006-4de253d81b95
golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91
+2 -2
View File
@@ -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-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
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.6/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
+194 -43
View File
@@ -68,22 +68,40 @@ type renderer struct {
pather *pather
packer packer
intersections packer
layers packer
layerFBOs fboSet
}
type drawOps struct {
profile bool
reader ops.Reader
states []f32.Affine2D
transStack []f32.Affine2D
vertCache []byte
viewport image.Point
clear bool
clearColor f32color.RGBA
imageOps []imageOp
pathOps []*pathOp
pathOpCache []pathOp
qs quadSplitter
pathCache *opCache
profile bool
reader ops.Reader
states []f32.Affine2D
transStack []f32.Affine2D
layers []opacityLayer
opacityStack []int
vertCache []byte
viewport image.Point
clear bool
clearColor f32color.RGBA
imageOps []imageOp
pathOps []*pathOp
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 {
@@ -127,7 +145,12 @@ type imageOp struct {
clip image.Rectangle
material material
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 {
@@ -154,10 +177,12 @@ type material struct {
// For materialTypeColor.
color f32color.RGBA
// For materialTypeLinearGradient.
color1 f32color.RGBA
color2 f32color.RGBA
color1 f32color.RGBA
color2 f32color.RGBA
opacity float32
// For materialTypeTexture.
data imageOpData
tex driver.Texture
uvTrans f32.Affine2D
}
@@ -222,8 +247,6 @@ func decodeLinearGradientOp(data []byte) linearGradientOpData {
}
}
type clipType uint8
type resource interface {
release()
}
@@ -273,6 +296,9 @@ type blitUniforms struct {
transform [4]float32
uvTransformR1 [4]float32
uvTransformR2 [4]float32
opacity float32
fbo float32
_ [2]float32
}
type colorUniforms struct {
@@ -284,7 +310,7 @@ type gradientUniforms struct {
color2 f32color.RGBA
}
type materialType uint8
type clipType uint8
const (
clipTypeNone clipType = iota
@@ -292,6 +318,8 @@ const (
clipTypeIntersection
)
type materialType uint8
const (
materialColor materialType = iota
materialLinearGradient
@@ -391,6 +419,8 @@ func (g *gpu) frame(target RenderTarget) error {
g.coverTimer.begin()
g.renderer.uploadImages(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{
ClearColor: g.drawOps.clearColor,
}
@@ -400,7 +430,7 @@ func (g *gpu) frame(target RenderTarget) error {
}
g.ctx.BeginRenderPass(defFBO, d)
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.ctx.EndRenderPass()
g.cleanupTimer.begin()
@@ -464,15 +494,18 @@ func newRenderer(ctx driver.Device) *renderer {
if cap := 8192; maxDim > cap {
maxDim = cap
}
d := image.Pt(maxDim, maxDim)
r.packer.maxDims = image.Pt(maxDim, maxDim)
r.intersections.maxDims = image.Pt(maxDim, maxDim)
r.packer.maxDims = d
r.intersections.maxDims = d
r.layers.maxDims = d
return r
}
func (r *renderer) release() {
r.pather.release()
r.blitter.release()
r.layerFBOs.delete(r.ctx, 0)
}
func newBlitter(ctx driver.Device) *blitter {
@@ -747,8 +780,7 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
ops = ops[:len(ops)-1]
continue
}
sz := image.Point{X: p.clip.Dx(), Y: p.clip.Dy()}
place, ok := r.packer.add(sz)
place, ok := r.packer.add(p.clip.Size())
if !ok {
// The clip area is at most the entire screen. Hopefully no
// screen is larger than GL_MAX_TEXTURE_SIZE.
@@ -760,6 +792,83 @@ func (r *renderer) packStencils(pops *[]*pathOp) {
*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) {
d.profile = false
d.viewport = viewport
@@ -768,6 +877,8 @@ func (d *drawOps) reset(viewport image.Point) {
d.pathOpCache = d.pathOpCache[:0]
d.vertCache = d.vertCache[: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) {
@@ -866,6 +977,27 @@ loop:
state.t = 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:
quads.key.strokeWidth = decodeStrokeOp(encOp.Data)
@@ -958,7 +1090,7 @@ loop:
mat := state.materialFor(bnd, off, partialTrans, bounds)
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.
// Scrap images up to and including this image and set clear color.
d.imageOps = d.imageOps[:0]
@@ -971,6 +1103,15 @@ loop:
clip: bounds,
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)
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 {
var m material
m := material{
opacity: 1.,
}
switch d.matType {
case 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) {
for _, img := range ops {
for i := range ops {
img := &ops[i]
m := img.material
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
switch m.material {
case materialTexture:
r.ctx.PrepareTexture(r.texHandle(cache, m.data))
r.ctx.PrepareTexture(m.tex)
}
var fbo stencilFBO
var fbo FBO
switch img.clipType {
case clipTypeNone:
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
for _, img := range ops {
for i := 0; i < len(ops); i++ {
img := ops[i]
i += img.layerOps
m := img.material
switch m.material {
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)
var fbo stencilFBO
scale, off := clipSpaceTransform(drc, viewport)
var fbo FBO
switch img.clipType {
case clipTypeNone:
p := r.blitter.pipelines[m.material]
r.ctx.BindPipeline(p.pipeline)
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
case clipTypePath:
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]
r.ctx.BindPipeline(p.pipeline)
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]
b.ctx.BindPipeline(p.pipeline)
var uniforms *blitUniforms
@@ -1119,18 +1265,23 @@ func (b *blitter) blit(mat materialType, col f32color.RGBA, col1, col2 f32color.
uniforms = &b.colUniforms.blitUniforms
case materialTexture:
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.uvTransformR1 = [4]float32{t1, t2, t3, 0}
uniforms.uvTransformR2 = [4]float32{t4, t5, t6, 0}
case materialLinearGradient:
b.linearGradientUniforms.color1 = col1
b.linearGradientUniforms.color2 = col2
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.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}
p.UploadUniforms(b.ctx)
b.ctx.DrawArrays(0, 4)
Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

+16
View File
@@ -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.
func lerp(a, b f32color.RGBA, p float32) f32color.RGBA {
return f32color.RGBA{
+16 -12
View File
@@ -58,7 +58,7 @@ type coverUniforms struct {
uvCoverTransform [4]float32
uvTransformR1 [4]float32
uvTransformR2 [4]float32
_ float32
fbo float32
}
type stenciler struct {
@@ -90,10 +90,10 @@ type intersectUniforms struct {
}
type fboSet struct {
fbos []stencilFBO
fbos []FBO
}
type stencilFBO struct {
type FBO struct {
size image.Point
tex driver.Texture
}
@@ -247,10 +247,10 @@ func newStenciler(ctx driver.Device) *stenciler {
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.
for i := len(s.fbos); i < len(sizes); i++ {
s.fbos = append(s.fbos, stencilFBO{})
s.fbos = append(s.fbos, FBO{})
}
// Resize fbos.
for i, sz := range sizes {
@@ -273,7 +273,7 @@ func (s *fboSet) resize(ctx driver.Device, sizes []image.Point) {
if 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)
if err != nil {
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
// floating point formats. Replace with GL_RGB+GL_UNSIGNED_BYTE if
// 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]
}
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) {
@@ -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) {
p.coverer.cover(mat, col, col1, col2, scale, off, uvTrans, coverScale, coverOff)
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, 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
switch mat {
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}
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.uvCoverTransform = [4]float32{coverScale.X, coverScale.Y, coverOff.X, coverOff.Y}
c.pipelines[mat].UploadUniforms(c.ctx)
+1
View File
@@ -62,6 +62,7 @@ func (c *Context) Release() {
eglDestroyContext(c.disp, c.eglCtx.ctx)
c.eglCtx = nil
}
eglTerminate(c.disp)
c.disp = nilEGLDisplay
}
+48 -5
View File
@@ -19,6 +19,17 @@ type Ops struct {
data []byte
// refs hold external references for operations.
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
// StateOp.
nextStateID int
@@ -40,9 +51,10 @@ const (
TypeMacro OpType = iota + firstOpIndex
TypeCall
TypeDefer
TypePushTransform
TypeTransform
TypePopTransform
TypePushOpacity
TypePopOpacity
TypeInvalidate
TypeImage
TypePaint
@@ -111,6 +123,7 @@ const (
ClipStack StackKind = iota
TransStack
PassStack
OpacityStack
_StackKind
)
@@ -124,9 +137,10 @@ const (
TypeMacroLen = 1 + 4 + 4
TypeCallLen = 1 + 4 + 4 + 4 + 4
TypeDeferLen = 1
TypePushTransformLen = 1 + 4*6
TypeTransformLen = 1 + 1 + 4*6
TypePopTransformLen = 1
TypePushOpacityLen = 1 + 4
TypePopOpacityLen = 1
TypeRedrawLen = 1 + 8
TypeImageLen = 1
TypePaintLen = 1
@@ -183,8 +197,12 @@ func Reset(o *Ops) {
for i := range o.refs {
o.refs[i] = nil
}
for i := range o.stringRefs {
o.stringRefs[i] = ""
}
o.data = o.data[:0]
o.refs = o.refs[:0]
o.stringRefs = o.stringRefs[:0]
o.nextStateID = 0
o.version++
}
@@ -265,12 +283,26 @@ func Write1(o *Ops, n int, ref1 interface{}) []byte {
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 {
o.data = append(o.data, make([]byte, n)...)
o.refs = append(o.refs, ref1, ref2)
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 {
o.data = append(o.data, make([]byte, n)...)
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
}
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.
func DecodeSave(data []byte) int {
if OpType(data[0]) != TypeSave {
@@ -381,9 +421,10 @@ var opProps = [0x100]opProp{
TypeMacro: {Size: TypeMacroLen, NumRefs: 0},
TypeCall: {Size: TypeCallLen, NumRefs: 1},
TypeDefer: {Size: TypeDeferLen, NumRefs: 0},
TypePushTransform: {Size: TypePushTransformLen, NumRefs: 0},
TypeTransform: {Size: TypeTransformLen, NumRefs: 0},
TypePopTransform: {Size: TypePopTransformLen, NumRefs: 0},
TypePushOpacity: {Size: TypePushOpacityLen, NumRefs: 0},
TypePopOpacity: {Size: TypePopOpacityLen, NumRefs: 0},
TypeInvalidate: {Size: TypeRedrawLen, NumRefs: 0},
TypeImage: {Size: TypeImageLen, NumRefs: 2},
TypePaint: {Size: TypePaintLen, NumRefs: 0},
@@ -440,12 +481,14 @@ func (t OpType) String() string {
return "Call"
case TypeDefer:
return "Defer"
case TypePushTransform:
return "PushTransform"
case TypeTransform:
return "Transform"
case TypePopTransform:
return "PopTransform"
case TypePushOpacity:
return "PushOpacity"
case TypePopOpacity:
return "PopOpacity"
case TypeInvalidate:
return "Invalidate"
case TypeImage:
+1 -1
View File
@@ -30,7 +30,7 @@ func (h ReadOp) 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)
}
+2 -3
View File
@@ -323,8 +323,7 @@ func (h InputOp) Add(o *op.Ops) {
if h.Tag == nil {
panic("Tag must be non-nil")
}
filter := h.Keys
data := ops.Write2(&o.Internal, ops.TypeKeyInputLen, h.Tag, &filter)
data := ops.Write2String(&o.Internal, ops.TypeKeyInputLen, h.Tag, string(h.Keys))
data[0] = byte(ops.TypeKeyInput)
data[1] = byte(h.Hint)
}
@@ -343,7 +342,7 @@ func (h FocusOp) 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)
bo := binary.LittleEndian
bo.PutUint32(data[1:], uint32(s.Range.Start))
+37 -23
View File
@@ -432,39 +432,42 @@ func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
return id.id
}
func (q *pointerQueue) ActionAt(pos f32.Point) (system.Action, bool) {
for i := len(q.hitTree) - 1; i >= 0; i-- {
n := &q.hitTree[i]
hit, _ := q.hit(n.area, pos)
if !hit {
continue
}
func (q *pointerQueue) ActionAt(pos f32.Point) (action system.Action, hasAction bool) {
q.hitTest(pos, func(n *hitNode) bool {
area := q.areas[n.area]
return area.action, area.action != 0
}
return 0, false
if area.action != 0 {
action = area.action
hasAction = true
return false
}
return true
})
return action, hasAction
}
func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) {
func (q *pointerQueue) SemanticAt(pos f32.Point) (semID SemanticID, hasSemID bool) {
q.assignSemIDs()
for i := len(q.hitTree) - 1; i >= 0; i-- {
n := &q.hitTree[i]
hit, _ := q.hit(n.area, pos)
if !hit {
continue
}
q.hitTest(pos, func(n *hitNode) bool {
area := q.areas[n.area]
if area.semantic.id != 0 {
return area.semantic.id, true
semID = area.semantic.id
hasSemID = true
return false
}
}
return 0, false
return true
})
return semID, hasSemID
}
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
// hitTest searches the hit tree for nodes matching pos. Any node matching pos will
// have the onNode func invoked on it to allow the caller to extract whatever information
// is necessary for further processing. onNode may return false to terminate the walk of
// the hit tree, or true to continue. Providing this algorithm in this generic way
// allows normal event routing and system action event routing to share the same traversal
// logic even though they are interested in different aspects of hit nodes.
func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode) bool) pointer.Cursor {
// Track whether we're passing through hits.
pass := true
hits := q.scratch[:0]
idx := len(q.hitTree) - 1
cursor := pointer.CursorDefault
for idx >= 0 {
@@ -483,12 +486,23 @@ func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
} else {
idx = n.next
}
if !onNode(n) {
break
}
}
return cursor
}
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
hits := q.scratch[:0]
cursor := q.hitTest(pos, func(n *hitNode) bool {
if n.tag != nil {
if _, exists := q.handlers[n.tag]; exists {
hits = addHandler(hits, n.tag)
}
}
}
return true
})
q.scratch = hits[:0]
return hits, cursor
}
+49
View File
@@ -14,6 +14,7 @@ import (
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer"
"gioui.org/op"
"gioui.org/op/clip"
@@ -221,6 +222,43 @@ func TestPointerTypes(t *testing.T) {
assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, pointer.Release)
}
func TestPointerSystemAction(t *testing.T) {
t.Run("simple", func(t *testing.T) {
var ops op.Ops
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionMove).Add(&ops)
r1.Pop()
var r Router
r.Frame(&ops)
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
})
t.Run("covered by another clip", func(t *testing.T) {
var ops op.Ops
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionMove).Add(&ops)
clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
r1.Pop()
var r Router
r.Frame(&ops)
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
})
t.Run("uses topmost action op", func(t *testing.T) {
var ops op.Ops
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionMove).Add(&ops)
r2 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionClose).Add(&ops)
r2.Pop()
r1.Pop()
var r Router
r.Frame(&ops)
assertActionAt(t, r, f32.Pt(50, 50), system.ActionClose)
})
}
func TestPointerPriority(t *testing.T) {
handler1 := new(int)
handler2 := new(int)
@@ -1231,6 +1269,17 @@ func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) {
}
}
// assertActionAt checks that the router has a system action of the expected type at point.
func assertActionAt(t *testing.T, q Router, point f32.Point, expected system.Action) {
t.Helper()
action, ok := q.ActionAt(point)
if !ok {
t.Errorf("expected action %v at %v, got no action", expected, point)
} else if action != expected {
t.Errorf("expected action %v at %v, got %v", expected, point, action)
}
}
func BenchmarkRouterAdd(b *testing.B) {
// Set this to the number of overlapping handlers that you want to
// evaluate performance for. Typical values for the example applications
+4 -4
View File
@@ -490,11 +490,11 @@ func (q *Router) collect() {
}
kc.softKeyboard(op.Show)
case ops.TypeKeyInput:
filter := encOp.Refs[1].(*key.Set)
filter := key.Set(*encOp.Refs[1].(*string))
op := key.InputOp{
Tag: encOp.Refs[0].(event.Tag),
Hint: key.InputHint(encOp.Data[1]),
Keys: *filter,
Keys: filter,
}
a := pc.currentArea()
b := pc.currentAreaBounds()
@@ -532,10 +532,10 @@ func (q *Router) collect() {
// Semantic ops.
case ops.TypeSemanticLabel:
lbl := encOp.Refs[0].(string)
lbl := *encOp.Refs[0].(*string)
pc.semanticLabel(lbl)
case ops.TypeSemanticDesc:
desc := encOp.Refs[0].(string)
desc := *encOp.Refs[0].(*string)
pc.semanticDesc(desc)
case ops.TypeSemanticClass:
class := semantic.ClassOp(encOp.Data[1])
+2 -2
View File
@@ -40,12 +40,12 @@ type SelectedOp bool
type DisabledOp bool
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)
}
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)
}
+6
View File
@@ -204,6 +204,9 @@ func (p *Path) Line(delta f32.Point) {
// LineTo moves the pen to the absolute point specified, recording a line.
func (p *Path) LineTo(to f32.Point) {
if to == p.pen {
return
}
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
bo := binary.LittleEndian
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
// with the control point ctrl, with absolute coordinates.
func (p *Path) QuadTo(ctrl, to f32.Point) {
if ctrl == p.pen && to == p.pen {
return
}
data := ops.WriteMulti(p.ops, scene.CommandSize+4)
bo := binary.LittleEndian
bo.PutUint32(data[0:], uint32(p.contour))
+36
View File
@@ -44,6 +44,14 @@ type LinearGradientOp 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 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)
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
View File
@@ -36,7 +36,8 @@ type document struct {
lines []line
alignment Alignment
// alignWidth is the width used when aligning text.
alignWidth int
alignWidth int
unreadRuneCount int
}
// append adds the lines of other to the end of l and ensures they
@@ -52,6 +53,7 @@ func (l *document) reset() {
l.lines = l.lines[:0]
l.alignment = Start
l.alignWidth = 0
l.unreadRuneCount = 0
}
func max(a, b int) int {
@@ -93,6 +95,55 @@ type line struct {
yOffset int
}
// insertTrailingSyntheticNewline adds a synthetic newline to the final logical run of the line
// with the given shaping cluster index.
func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) {
// If there was a newline at the end of this paragraph, insert a synthetic glyph representing it.
finalContentRun := len(l.runs) - 1
// If there was a trailing newline update the rune counts to include
// it on the last line of the paragraph.
l.runeCount += 1
l.runs[finalContentRun].Runes.Count += 1
syntheticGlyph := glyph{
id: 0,
clusterIndex: newLineClusterIdx,
glyphCount: 0,
runeCount: 1,
xAdvance: 0,
yAdvance: 0,
xOffset: 0,
yOffset: 0,
}
// Inset the synthetic newline glyph on the proper end of the run.
if l.runs[finalContentRun].Direction.Progression() == system.FromOrigin {
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, syntheticGlyph)
} else {
// Ensure capacity.
l.runs[finalContentRun].Glyphs = append(l.runs[finalContentRun].Glyphs, glyph{})
copy(l.runs[finalContentRun].Glyphs[1:], l.runs[finalContentRun].Glyphs)
l.runs[finalContentRun].Glyphs[0] = syntheticGlyph
}
}
func (l *line) setTruncatedCount(truncatedCount int) {
// If we've truncated the text with a truncator, adjust the rune counts within the
// truncator to make it represent the truncated text.
finalRunIdx := len(l.runs) - 1
l.runs[finalRunIdx].truncator = true
finalGlyphIdx := len(l.runs[finalRunIdx].Glyphs) - 1
// The run represents all of the truncated text.
l.runs[finalRunIdx].Runes.Count = truncatedCount
// Only the final glyph represents any runes, and it represents all truncated text.
for i := range l.runs[finalRunIdx].Glyphs {
if i == finalGlyphIdx {
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncatedCount
} else {
l.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
}
}
}
// Range describes the position and quantity of a range of text elements
// within a larger slice. The unit is usually runes of unicode data or
// glyphs of shaped font data.
@@ -528,35 +579,21 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
if hasNewline {
txt = txt[:len(txt)-1]
}
truncatedNewline := false
if hasNewline && len(txt) == 0 {
params.forceTruncate = false
// If we only have a newline, shape a space to get line metrics.
ls, truncated = s.shapeAndWrapText(params, []rune{' '})
if truncated > 0 {
// Our space was truncated. Since our space didn't exist in any meaningful
// capacity, ensure the truncated count is zeroed out.
truncated = 0
truncatedNewline = true
} else {
// We shaped a space to get proper line metrics, but we need to drop
// the rune/glyph info since it isn't actually part of the text.
ls[0][0].Glyphs = ls[0][0].Glyphs[:0]
ls[0][0].Advance = 0
ls[0][0].Runes.Count = 0
}
} else {
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
if params.MaxLines != 0 && hasNewline {
// If we might end up truncating a trailing newline, we must insert the truncator symbol
// on the final line (if we hit the limit).
params.forceTruncate = true
}
ls, truncated = s.shapeAndWrapText(params, replaceControlCharacters(txt))
didTruncate := truncated > 0 || truncatedNewline || (params.forceTruncate && params.MaxLines == len(ls))
if didTruncate && hasNewline {
// We've truncated the newline, since it was at the end and we've truncated some amount of runes
// before it.
hasTruncator := truncated > 0 || (params.forceTruncate && params.MaxLines == len(ls))
if hasTruncator && hasNewline {
// We have a truncator at the end of the line, so the newline is logically
// truncated as well.
truncated++
hasNewline = false
}
// Convert to Lines.
textLines := make([]line, len(ls))
maxHeight := fixed.Int26_6(0)
@@ -565,49 +602,12 @@ func (s *shaperImpl) LayoutRunes(params Parameters, txt []rune) document {
if otLine.lineHeight > maxHeight {
maxHeight = otLine.lineHeight
}
isFinalLine := i == len(ls)-1
if isFinalLine && hasNewline {
// If there was a trailing newline update the rune counts to include
// it on the last line of the paragraph.
finalRunIdx := len(otLine.runs) - 1
otLine.runeCount += 1
otLine.runs[finalRunIdx].Runes.Count += 1
syntheticGlyph := glyph{
id: 0,
clusterIndex: len(txt),
glyphCount: 0,
runeCount: 1,
xAdvance: 0,
yAdvance: 0,
xOffset: 0,
yOffset: 0,
if isFinalLine := i == len(ls)-1; isFinalLine {
if hasNewline {
otLine.insertTrailingSyntheticNewline(len(txt))
}
// Inset the synthetic newline glyph on the proper end of the run.
if otLine.runs[finalRunIdx].Direction.Progression() == system.FromOrigin {
otLine.runs[finalRunIdx].Glyphs = append(otLine.runs[finalRunIdx].Glyphs, syntheticGlyph)
} else {
// Ensure capacity.
otLine.runs[finalRunIdx].Glyphs = append(otLine.runs[finalRunIdx].Glyphs, glyph{})
copy(otLine.runs[finalRunIdx].Glyphs[1:], otLine.runs[finalRunIdx].Glyphs)
otLine.runs[finalRunIdx].Glyphs[0] = syntheticGlyph
}
}
if isFinalLine && didTruncate {
// If we've truncated the text with a truncator, adjust the rune counts within the
// truncator to make it represent the truncated text.
finalRunIdx := len(otLine.runs) - 1
otLine.runs[finalRunIdx].truncator = true
finalGlyphIdx := len(otLine.runs[finalRunIdx].Glyphs) - 1
// The run represents all of the truncated text.
otLine.runs[finalRunIdx].Runes.Count = truncated
// Only the final glyph represents any runes, and it represents all truncated text.
for i := range otLine.runs[finalRunIdx].Glyphs {
if i == finalGlyphIdx {
otLine.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = truncated
} else {
otLine.runs[finalRunIdx].Glyphs[finalGlyphIdx].runeCount = 0
}
if hasTruncator {
otLine.setTruncatedCount(truncated)
}
}
textLines[i] = otLine
+9 -7
View File
@@ -245,8 +245,11 @@ func WithCollection(collection []FontFace) ShaperOption {
}
}
// NewShaper constructs a shaper with the provided collection of font faces
// available.
// NewShaper constructs a shaper with the provided options.
//
// NewShaper must be called after [app.NewWindow], unless the [NoSystemFonts]
// option is specified. This is an unfortunate restriction caused by some platforms
// such as Android.
func NewShaper(options ...ShaperOption) *Shaper {
l := &Shaper{}
for _, opt := range options {
@@ -351,11 +354,7 @@ func (l *Shaper) layoutText(params Parameters, txt io.Reader, str string) {
unreadRunes++
}
}
lastLineIdx := len(lines.lines) - 1
lastRunIdx := len(lines.lines[lastLineIdx].runs) - 1
lastGlyphIdx := len(lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs) - 1
lines.lines[lastLineIdx].runs[lastRunIdx].Runes.Count += unreadRunes
lines.lines[lastLineIdx].runs[lastRunIdx].Glyphs[lastGlyphIdx].runeCount += unreadRunes
l.txt.unreadRuneCount = unreadRunes
}
}
l.txt.append(lines)
@@ -505,6 +504,9 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
}
if endOfCluster {
glyph.Flags |= FlagClusterBreak
if run.truncator {
glyph.Runes += l.txt.unreadRuneCount
}
} else {
glyph.Runes = 0
}
+36 -16
View File
@@ -154,9 +154,11 @@ func TestWrappingForcedTruncation(t *testing.T) {
// consistently and does not create spurious lines of text.
func TestShapingNewlineHandling(t *testing.T) {
type testcase struct {
textInput string
expectedLines int
expectedGlyphs int
textInput string
expectedLines int
expectedGlyphs int
maxLines int
expectedTruncated int
}
for _, tc := range []testcase{
{textInput: "a\n", expectedLines: 1, expectedGlyphs: 3},
@@ -165,21 +167,41 @@ func TestShapingNewlineHandling(t *testing.T) {
{textInput: "\n", expectedLines: 1, expectedGlyphs: 2},
{textInput: "\n\n", expectedLines: 2, expectedGlyphs: 3},
{textInput: "\n\n\n", expectedLines: 3, expectedGlyphs: 4},
{textInput: "\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 1},
{textInput: "\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 2},
{textInput: "\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 1, expectedTruncated: 3},
{textInput: "a\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 1},
{textInput: "a\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 2},
{textInput: "a\n\n\n", expectedLines: 1, maxLines: 1, expectedGlyphs: 2, expectedTruncated: 3},
{textInput: "\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 2},
{textInput: "\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 1},
{textInput: "\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 2, expectedTruncated: 2},
{textInput: "a\n", expectedLines: 1, maxLines: 2, expectedGlyphs: 3},
{textInput: "a\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 1},
{textInput: "a\n\n\n", expectedLines: 2, maxLines: 2, expectedGlyphs: 3, expectedTruncated: 2},
} {
t.Run(fmt.Sprintf("%q", tc.textInput), func(t *testing.T) {
t.Run(fmt.Sprintf("%q-maxLines%d", tc.textInput, tc.maxLines), func(t *testing.T) {
ltrFace, _ := opentype.Parse(goregular.TTF)
collection := []FontFace{{Face: ltrFace}}
cache := NewShaper(NoSystemFonts(), WithCollection(collection))
checkGlyphs := func() {
glyphs := []Glyph{}
runes := 0
truncated := 0
for g, ok := cache.NextGlyph(); ok; g, ok = cache.NextGlyph() {
glyphs = append(glyphs, g)
runes += g.Runes
if g.Flags&FlagTruncator == 0 {
runes += g.Runes
} else {
truncated += g.Runes
}
}
if expected := len([]rune(tc.textInput)); expected != runes {
if expected := len([]rune(tc.textInput)) - tc.expectedTruncated; expected != runes {
t.Errorf("expected %d runes, got %d", expected, runes)
}
if truncated != tc.expectedTruncated {
t.Errorf("expected %d truncated runes, got %d", tc.expectedTruncated, truncated)
}
if len(glyphs) != tc.expectedGlyphs {
t.Errorf("expected %d glyphs, got %d", tc.expectedGlyphs, len(glyphs))
}
@@ -207,29 +229,27 @@ func TestShapingNewlineHandling(t *testing.T) {
t.Errorf("expected paragraph start glyph to have cursor y")
}
}
if count := strings.Count(tc.textInput, "\n"); found != count {
if count := strings.Count(tc.textInput, "\n"); found != count && tc.maxLines == 0 {
t.Errorf("expected %d paragraph breaks, found %d", count, found)
} else if tc.maxLines > 0 && found > tc.maxLines {
t.Errorf("expected %d paragraph breaks due to truncation, found %d", tc.maxLines, found)
}
}
cache.LayoutString(Parameters{
params := Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, tc.textInput)
MaxLines: tc.maxLines,
}
cache.LayoutString(params, tc.textInput)
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
t.Errorf("shaping string %q created %d lines", tc.textInput, lineCount)
}
checkGlyphs()
cache.Layout(Parameters{
Alignment: Middle,
PxPerEm: fixed.I(10),
MinWidth: 200,
MaxWidth: 200,
Locale: english,
}, strings.NewReader(tc.textInput))
cache.Layout(params, strings.NewReader(tc.textInput))
if lineCount := len(cache.txt.lines); lineCount > tc.expectedLines {
t.Errorf("shaping reader %q created %d lines", tc.textInput, lineCount)
}
+19 -5
View File
@@ -28,6 +28,7 @@ type Clickable struct {
keyTag struct{}
requestFocus bool
focused bool
pressedKey string
}
// Click represents a click.
@@ -178,17 +179,30 @@ func (b *Clickable) update(gtx layout.Context) {
switch e := e.(type) {
case key.FocusEvent:
b.focused = e.Focus
if !b.focused {
b.pressedKey = ""
}
case key.Event:
if !b.focused || e.State != key.Release {
if !b.focused {
break
}
if e.Name != key.NameReturn && e.Name != key.NameSpace {
break
}
b.clicks = append(b.clicks, Click{
Modifiers: e.Modifiers,
NumClicks: 1,
})
switch e.State {
case key.Press:
b.pressedKey = e.Name
case key.Release:
if b.pressedKey != e.Name {
break
}
// only register a key as a click if the key was pressed and released while this button was focused
b.pressedKey = ""
b.clicks = append(b.clicks, Click{
Modifiers: e.Modifiers,
NumClicks: 1,
})
}
}
}
}
+104
View File
@@ -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")
}
}