Compare commits

..

2 Commits

Author SHA1 Message Date
Joe Julian d9b0c8c1c5 app: avoid relocking contexts every frame
Signed-off-by: Joe Julian <me@joejulian.name>
2026-04-22 19:08:26 -07:00
Joe Julian 7bb7a1407f app: lock explicitly before refreshing contexts
Signed-off-by: Joe Julian <me@joejulian.name>
2026-04-21 06:31:35 -07:00
22 changed files with 163 additions and 326 deletions
-3
View File
@@ -115,9 +115,6 @@ func (c *context) Unlock() {
}
func (c *context) Refresh() error {
if C.gio_makeCurrent(c.ctx) == 0 {
return errors.New("[EAGLContext setCurrentContext] failed")
}
if !c.init {
c.init = true
c.frameBuffer = c.c.CreateFramebuffer()
-2
View File
@@ -111,8 +111,6 @@ func (c *glContext) Unlock() {
}
func (c *glContext) Refresh() error {
c.Lock()
defer c.Unlock()
C.gio_updateContext(c.ctx)
return nil
}
+2 -4
View File
@@ -207,9 +207,7 @@ const (
CFS_POINT = 0x0002
CFS_CANDIDATEPOS = 0x0040
HWND_TOP = syscall.Handle(0)
HWND_TOPMOST = ^(syscall.Handle(1) - 1) // -1
HWND_NOTOPMOST = ^(syscall.Handle(2) - 1) // -2
HWND_TOPMOST = ^(uint32(1) - 1) // -1
HTCAPTION = 2
HTCLIENT = 1
@@ -784,7 +782,7 @@ func SetWindowPlacement(hwnd syscall.Handle, wp *WindowPlacement) {
_SetWindowPlacement.Call(uintptr(hwnd), uintptr(unsafe.Pointer(wp)))
}
func SetWindowPos(hwnd, hwndInsertAfter syscall.Handle, x, y, dx, dy int32, style uintptr) {
func SetWindowPos(hwnd syscall.Handle, hwndInsertAfter uint32, x, y, dx, dy int32, style uintptr) {
_SetWindowPos.Call(uintptr(hwnd), uintptr(hwndInsertAfter),
uintptr(x), uintptr(y),
uintptr(dx), uintptr(dy),
+2
View File
@@ -165,6 +165,8 @@ type frameEvent struct {
Sync bool
}
// The caller must hold the context lock while using API, Refresh,
// RenderTarget, or Present.
type context interface {
API() gpu.API
RenderTarget() (gpu.RenderTarget, error)
+41 -54
View File
@@ -369,7 +369,8 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
w.update()
case windows.WM_WINDOWPOSCHANGED:
w.update()
return 0
case windows.WM_SIZE:
w.update()
case windows.WM_GETMINMAXINFO:
mm := (*windows.MinMaxInfo)(unsafe.Pointer(lParam))
@@ -412,7 +413,6 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
icaret := image.Pt(int(caret.X+.5), int(caret.Y+.5))
windows.ImmSetCompositionWindow(imc, icaret.X, icaret.Y)
windows.ImmSetCandidateWindow(imc, icaret.X, icaret.Y)
return windows.TRUE
case windows.WM_IME_COMPOSITION:
imc := windows.ImmGetContext(w.hwnd)
if imc == 0 {
@@ -495,32 +495,34 @@ func getModifiers() key.Modifiers {
// hitTest returns the non-client area hit by the point, needed to
// process WM_NCHITTEST.
func (w *window) hitTest(x, y int) uintptr {
if w.config.Mode == Windowed {
// 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 {
case top && left:
return windows.HTTOPLEFT
case top && right:
return windows.HTTOPRIGHT
case bottom && left:
return windows.HTBOTTOMLEFT
case bottom && right:
return windows.HTBOTTOMRIGHT
case top:
return windows.HTTOP
case bottom:
return windows.HTBOTTOM
case left:
return windows.HTLEFT
case right:
return windows.HTRIGHT
}
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 {
case top && left:
return windows.HTTOPLEFT
case top && right:
return windows.HTTOPRIGHT
case bottom && left:
return windows.HTBOTTOMLEFT
case bottom && right:
return windows.HTBOTTOMRIGHT
case top:
return windows.HTTOP
case bottom:
return windows.HTBOTTOM
case left:
return windows.HTLEFT
case right:
return windows.HTRIGHT
}
p := f32.Pt(float32(x), float32(y))
if a, ok := w.w.ActionAt(p); ok && a == system.ActionMove {
@@ -699,13 +701,7 @@ func (w *window) ReadClipboard() {
w.readClipboard()
}
func (w *window) readClipboard() (cerr error) {
defer func() {
if cerr != nil {
w.processDataEvent("")
}
}()
func (w *window) readClipboard() error {
if err := windows.OpenClipboard(w.hwnd); err != nil {
return err
}
@@ -720,17 +716,13 @@ func (w *window) readClipboard() (cerr error) {
}
defer windows.GlobalUnlock(mem)
content := gowindows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr)))
w.processDataEvent(content)
return nil
}
func (w *window) processDataEvent(content string) {
w.ProcessEvent(transfer.DataEvent{
Type: "application/text",
Open: func() io.ReadCloser {
return io.NopCloser(strings.NewReader(content))
},
})
return nil
}
func (w *window) Configure(options []Option) {
@@ -747,16 +739,7 @@ func (w *window) Configure(options []Option) {
style := windows.GetWindowLong(w.hwnd, windows.GWL_STYLE)
var showMode int32
var x, y, width, height int32
swpStyle := uintptr(windows.SWP_FRAMECHANGED)
if cnf.TopMost == w.config.TopMost {
// Don't change the z-order if TopMost didn't change.
swpStyle |= windows.SWP_NOZORDER
}
hwndAfter := windows.HWND_NOTOPMOST
if cnf.TopMost {
hwndAfter = windows.HWND_TOPMOST
}
w.config.TopMost = cnf.TopMost
swpStyle := uintptr(windows.SWP_NOZORDER | windows.SWP_FRAMECHANGED)
winStyle := uintptr(windows.WS_OVERLAPPEDWINDOW)
style &^= winStyle
switch cnf.Mode {
@@ -808,7 +791,7 @@ func (w *window) Configure(options []Option) {
// Note: these invocation all trigger the windows callback method which may process a pending system.ActionCenter
// action, so SetWindowPos should come first so as to not "overwrite" system.ActionCenter.
windows.SetWindowPos(w.hwnd, hwndAfter, x, y, width, height, swpStyle)
windows.SetWindowPos(w.hwnd, 0, x, y, width, height, swpStyle)
windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style)
windows.ShowWindow(w.hwnd, showMode)
}
@@ -929,15 +912,19 @@ func (w *window) Perform(acts system.Action) {
y := (mi.Bottom - mi.Top - dy) / 2
windows.SetWindowPos(w.hwnd, 0, x, y, dx, dy, windows.SWP_NOZORDER|windows.SWP_FRAMECHANGED)
case system.ActionRaise:
windows.SetForegroundWindow(w.hwnd)
windows.SetWindowPos(w.hwnd, windows.HWND_TOP, 0, 0, 0, 0,
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
w.raise()
case system.ActionClose:
windows.PostMessage(w.hwnd, windows.WM_CLOSE, 0, 0)
}
})
}
func (w *window) raise() {
windows.SetForegroundWindow(w.hwnd)
windows.SetWindowPos(w.hwnd, windows.HWND_TOPMOST, 0, 0, 0, 0,
windows.SWP_NOMOVE|windows.SWP_NOSIZE|windows.SWP_SHOWWINDOW)
}
func convertKeyCode(code uintptr) (key.Name, bool) {
if '0' <= code && code <= '9' || 'A' <= code && code <= 'Z' {
return key.Name(rune(code)), true
+42 -14
View File
@@ -46,6 +46,10 @@ type Window struct {
ctx context
gpu gpu.GPU
// ctxNeedsLock tracks whether the rendering context must be made
// current again before the next GPU operation. Refresh paths, surface
// loss, and explicit unlocks all invalidate the current binding.
ctxNeedsLock bool
// timer tracks the delayed invalidate goroutine.
timer struct {
// quit is shuts down the goroutine.
@@ -146,9 +150,14 @@ func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops,
if err != nil {
return err
}
w.ctxNeedsLock = true
sync = true
}
}
if err := w.lockContext(); err != nil {
w.destroyGPU()
return err
}
if sync && w.ctx != nil {
if err := w.ctx.Refresh(); err != nil {
if errors.Is(err, errOutOfDate) {
@@ -162,9 +171,8 @@ func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops,
}
return err
}
}
if w.ctx != nil {
if err := w.ctx.Lock(); err != nil {
w.unlockContext()
if err := w.lockContext(); err != nil {
w.destroyGPU()
return err
}
@@ -172,7 +180,7 @@ func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops,
if w.gpu == nil && !w.nocontext {
gpu, err := gpu.New(w.ctx.API())
if err != nil {
w.ctx.Unlock()
w.unlockContext()
w.destroyGPU()
return err
}
@@ -180,7 +188,7 @@ func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops,
}
if w.gpu != nil {
if err := w.frame(frame, size); err != nil {
w.ctx.Unlock()
w.unlockContext()
if errors.Is(err, errOutOfDate) {
// GPU surface needs refreshing.
sync = true
@@ -200,7 +208,6 @@ func (w *Window) validateAndProcess(size image.Point, sync bool, frame *op.Ops,
var err error
if w.gpu != nil {
err = w.ctx.Present()
w.ctx.Unlock()
}
return err
}
@@ -439,7 +446,6 @@ func (c *callbacks) EditorState() editorState {
func (c *callbacks) SetComposingRegion(r key.Range) {
c.w.imeState.compose = r
c.w.driver.ProcessEvent(key.CompositionEvent(r))
}
func (c *callbacks) EditorInsert(text string) {
@@ -504,16 +510,37 @@ func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) {
return c.w.queue.ActionAt(p)
}
func (w *Window) lockContext() error {
if w.ctx == nil || !w.ctxNeedsLock {
return nil
}
if err := w.ctx.Lock(); err != nil {
return err
}
w.ctxNeedsLock = false
return nil
}
func (w *Window) unlockContext() {
if w.ctx == nil || w.ctxNeedsLock {
return
}
w.ctx.Unlock()
w.ctxNeedsLock = true
}
func (w *Window) destroyGPU() {
if w.gpu != nil {
w.ctx.Lock()
w.gpu.Release()
w.ctx.Unlock()
if err := w.lockContext(); err == nil {
w.gpu.Release()
w.unlockContext()
}
w.gpu = nil
}
if w.ctx != nil {
w.ctx.Release()
w.ctx = nil
w.ctxNeedsLock = false
}
}
@@ -656,10 +683,11 @@ func (w *Window) processEvent(e event.Event) bool {
w.coalesced.destroy = &e2
case ViewEvent:
if !e2.Valid() && w.gpu != nil {
w.ctx.Lock()
w.gpu.Release()
if err := w.lockContext(); err == nil {
w.gpu.Release()
w.unlockContext()
}
w.gpu = nil
w.ctx.Unlock()
}
w.coalesced.view = &e2
case ConfigEvent:
@@ -972,7 +1000,7 @@ func Decorated(enabled bool) Option {
// TopMost windows will be rendered above all other non-top-most windows.
//
// TopMost windows are supported on macOS, Windows.
// TopMost windows are only supported on MacOS currently.
func TopMost(enabled bool) Option {
return func(_ unit.Metric, cnf *Config) {
cnf.TopMost = enabled
+1 -1
View File
@@ -106,7 +106,7 @@ func parseLoader(ld *opentype.Loader) (*fontapi.Font, giofont.Font, error) {
// Face many be invoked any number of times and is safe so long as each return value is
// only used by one goroutine.
func (f Face) Face() *fontapi.Face {
return fontapi.NewFace(f.face)
return &fontapi.Face{Font: f.face}
}
// FontFace returns a text.Font with populated font metadata for the
+12 -3
View File
@@ -61,8 +61,12 @@ func (h *Hover) Update(q input.Source) bool {
h.entered = false
}
case pointer.Enter:
h.pid = e.PointerID
h.entered = true
if !h.entered {
h.pid = e.PointerID
}
if h.pid == e.PointerID {
h.entered = true
}
}
}
return h.entered
@@ -218,7 +222,12 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary {
break
}
c.pid = e.PointerID
if !c.hovered {
c.pid = e.PointerID
}
if c.pid != e.PointerID {
break
}
c.pressed = true
if e.Time-c.clickedAt < doubleClickDuration {
c.clicks++
-72
View File
@@ -100,78 +100,6 @@ func TestMouseClicks(t *testing.T) {
}
}
func TestClickPointerIDReassignment(t *testing.T) {
// A Click must accept a Press from a PointerID that differs from the
// one its hovered state was previously associated with. Some backends
// reassign a single physical pointer's ID over its lifetime — e.g. the
// Windows pointer API across focus changes — and locking the gesture
// to the first observed ID would silently drop every subsequent press.
//
// The sequence below puts the gesture into the buggy state through
// public events alone: a press under PointerID 1 starts an active
// press cycle, a Move under PointerID 2 arrives mid-press (which the
// router routes as an Enter for PID 2 but the gesture's Enter handler
// is a no-op for pid while pressed), then PID 1 releases. After this,
// the router has the gesture entered for PID 2 (so the next event
// under PID 2 won't trigger another Enter) but the gesture itself
// still has pid=1.
var click Click
var ops op.Ops
rect := image.Rect(0, 0, 100, 100)
stack := clip.Rect(rect).Push(&ops)
click.Add(&ops)
stack.Pop()
var r input.Router
click.Update(r.Source())
r.Frame(&ops)
drain := func() {
for {
if _, ok := click.Update(r.Source()); !ok {
return
}
}
}
// Press under PointerID 1.
r.Queue(
pointer.Event{Kind: pointer.Move, Source: pointer.Mouse, Position: f32.Pt(50, 50), PointerID: 1},
pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Buttons: pointer.ButtonPrimary, Position: f32.Pt(50, 50), PointerID: 1},
)
drain()
// Move under PointerID 2 while PointerID 1 is still pressed. The
// router records the gesture as entered for PointerID 2 but the
// gesture's Enter handler is a no-op for pid because c.pressed.
r.Queue(pointer.Event{Kind: pointer.Move, Source: pointer.Mouse, Position: f32.Pt(50, 50), PointerID: 2})
drain()
// Release PointerID 1. PointerID 1's press tracking ends; the
// gesture's recorded pid stays at 1.
r.Queue(pointer.Event{Kind: pointer.Release, Source: pointer.Mouse, Position: f32.Pt(50, 50), PointerID: 1})
drain()
// Press under PointerID 2. The router won't refire Enter for PID 2
// (the gesture is already in PID 2's entered set), so the gesture's
// only chance to refresh its pid is the Press handler itself.
r.Queue(pointer.Event{Kind: pointer.Press, Source: pointer.Mouse, Buttons: pointer.ButtonPrimary, Position: f32.Pt(50, 50), PointerID: 2})
var sawPress bool
for {
ev, ok := click.Update(r.Source())
if !ok {
break
}
if ev.Kind == KindPress {
sawPress = true
}
}
if !sawPress {
t.Fatal("expected KindPress for press under reassigned PointerID; gesture dropped the press because of stale recorded pid")
}
}
func mouseClickEvents(times ...time.Duration) []event.Event {
press := pointer.Event{
Kind: pointer.Press,
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.24.0
require (
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d
gioui.org/shader v1.0.8
github.com/go-text/typesetting v0.3.4
github.com/go-text/typesetting v0.3.0
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0
golang.org/x/image v0.26.0
+4 -4
View File
@@ -3,10 +3,10 @@ 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/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.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4=
github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 h1:tMSqXTK+AQdW3LpCbfatHSRPHeW6+2WuxaVQuHftn80=
+6 -2
View File
@@ -41,10 +41,14 @@ var (
_eglWaitClient *syscall.Proc
)
var loadOnce = sync.OnceValue(loadDLLs)
var loadOnce sync.Once
func loadEGL() error {
return loadOnce()
var err error
loadOnce.Do(func() {
err = loadDLLs()
})
return err
}
func loadDLLs() error {
+4 -19
View File
@@ -739,10 +739,6 @@ func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState,
state.pointers = nil
return state, evts
}
if e.Kind == pointer.Scroll {
// Scroll events are not bound to a pointer; see pointer.Event.PointerID.
return state, q.deliverScrollEvent(handlers, evts, e)
}
state, pidx := state.pointerOf(e)
p := state.pointers[pidx]
@@ -760,13 +756,14 @@ func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState,
if p.pressed {
p, evts = q.deliverDragEvent(handlers, p, evts)
}
case pointer.Leave:
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
case pointer.Release:
evts = q.deliverEvent(handlers, p, evts, e)
p.pressed = false
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
p, evts = q.deliverDropEvent(handlers, p, evts)
case pointer.Scroll:
p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e)
evts = q.deliverEvent(handlers, p, evts, e)
default:
panic("unsupported pointer event type")
}
@@ -783,18 +780,6 @@ func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState,
return state, evts
}
// deliverScrollEvent delivers scroll events to the handlers hit by the event coordinate.
func (q *pointerQueue) deliverScrollEvent(handlers map[event.Tag]*handler, evts []taggedEvent, e pointer.Event) []taggedEvent {
var hits []event.Tag
q.hitTest(e.Position, func(n *hitNode) bool {
if _, ok := handlers[n.tag]; ok {
hits = addHandler(hits, n.tag)
}
return true
})
return q.deliverEvent(handlers, pointerInfo{handlers: hits}, evts, e)
}
func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerInfo, evts []taggedEvent, e pointer.Event) []taggedEvent {
if p.pressed && len(p.handlers) == 1 {
e.Priority = pointer.Grabbed
@@ -825,7 +810,7 @@ func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerIn
func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler, cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) {
changed := false
var hits []event.Tag
if e.Kind == pointer.Leave || e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press {
if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press {
// Consider non-mouse pointers leaving when they're released.
} else {
var transSrc *pointerFilter
-76
View File
@@ -255,45 +255,6 @@ func TestPointerMove(t *testing.T) {
assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Enter, pointer.Move, pointer.Leave, pointer.Cancel)
}
func TestPointerLeave(t *testing.T) {
handler := new(int)
var ops op.Ops
filter := pointer.Filter{
Target: handler,
Kinds: pointer.Move | pointer.Enter | pointer.Leave | pointer.Cancel,
}
defer clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
event.Op(&ops, handler)
var r Router
events(&r, -1, filter)
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
PointerID: 1,
Position: f32.Pt(50, 50),
},
pointer.Event{
Kind: pointer.Leave,
Source: pointer.Mouse,
PointerID: 1,
Position: f32.Pt(50, 50),
},
)
assertEventPointerTypeSequence(t, events(&r, -1, filter), pointer.Enter, pointer.Move, pointer.Leave)
r.Queue(pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
PointerID: 1,
Position: f32.Pt(50, 50),
})
assertEventPointerTypeSequence(t, events(&r, -1, filter), pointer.Enter, pointer.Move)
}
func TestPointerTypes(t *testing.T) {
handler := new(int)
var ops op.Ops
@@ -1384,40 +1345,3 @@ func events(r *Router, n int, filters ...event.Filter) []event.Event {
}
return events
}
// TestPointerScrollDoesNotTrackPointer queues two events over two cursor
// regions. The Move puts the live pointer over the button (CursorPointer);
// the Scroll happens over the cell (CursorText) and must not update the
// cursor.
func TestPointerScrollDoesNotTrackPointer(t *testing.T) {
var ops op.Ops
button := clip.Rect(image.Rect(0, 0, 50, 50)).Push(&ops)
pointer.CursorPointer.Add(&ops)
button.Pop()
cell := clip.Rect(image.Rect(100, 0, 200, 50)).Push(&ops)
pointer.CursorText.Add(&ops)
cell.Pop()
var r Router
r.Frame(&ops)
r.Queue(
pointer.Event{
Kind: pointer.Move,
Source: pointer.Mouse,
Position: f32.Pt(25, 25),
},
pointer.Event{
Kind: pointer.Scroll,
Source: pointer.Mouse,
Position: f32.Pt(150, 25),
Scroll: f32.Pt(0, 1),
},
)
if got, want := r.Cursor(), pointer.CursorPointer; got != want {
t.Errorf("got %q, want %q (scroll position must not update the cursor; "+
"the live pointer's last position is what determines it)", got, want)
}
}
+1 -8
View File
@@ -419,7 +419,7 @@ func (f *filter) Merge(f2 filter) {
func (f *filter) Matches(e event.Event) bool {
switch e.(type) {
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent, key.CompositionEvent:
case key.FocusEvent, key.SnippetEvent, key.EditEvent, key.SelectionEvent:
return f.focusable
default:
return f.pointer.Matches(e)
@@ -463,13 +463,6 @@ func (q *Router) processEvent(e event.Event, system bool) {
evts = append(evts, taggedEvent{tag: f, event: e})
}
q.changeState(e, state, evts)
case key.CompositionEvent:
e = key.CompositionEvent(rangeNorm(key.Range(e)))
var evts []taggedEvent
if f := state.focus; f != nil {
evts = append(evts, taggedEvent{tag: f, event: e})
}
q.changeState(e, state, evts)
case key.EditEvent, key.FocusEvent, key.SelectionEvent:
var evts []taggedEvent
if f := state.focus; f != nil {
+5 -9
View File
@@ -77,9 +77,6 @@ type Caret struct {
// SelectionEvent is generated when an input method changes the selection.
type SelectionEvent Range
// CompositionEvent is generated when an input method changes the composing range.
type CompositionEvent Range
// SnippetEvent is generated when the snippet range is updated by an
// input method.
type SnippetEvent Range
@@ -246,12 +243,11 @@ func (h InputHintOp) Add(o *op.Ops) {
data[1] = byte(h.Hint)
}
func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {}
func (CompositionEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {}
func (EditEvent) ImplementsEvent() {}
func (Event) ImplementsEvent() {}
func (FocusEvent) ImplementsEvent() {}
func (SnippetEvent) ImplementsEvent() {}
func (SelectionEvent) ImplementsEvent() {}
func (FocusCmd) ImplementsCommand() {}
func (SoftKeyboardCmd) ImplementsCommand() {}
+1 -3
View File
@@ -19,9 +19,7 @@ type Event struct {
Source Source
// PointerID is the id for the pointer and can be used
// to track a particular pointer from Press to
// Release. Populated for Press, Release, Move, Drag,
// Enter, Leave, and Cancel; Scroll events are not
// bound to a tracked pointer and leave it zero.
// Release.
PointerID ID
// Priority is the priority of the receiving handler
// for this event.
+14 -12
View File
@@ -103,7 +103,8 @@ func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) {
clusterIndex: newLineClusterIdx,
glyphCount: 0,
runeCount: 1,
advance: 0,
xAdvance: 0,
yAdvance: 0,
xOffset: 0,
yOffset: 0,
}
@@ -159,9 +160,9 @@ type glyph struct {
// runeCount is the quantity of runes in the source text that this glyph
// corresponds to.
runeCount int
// advance is the distance the dot moves when laying out the glyph along
// the run's primary axis.
advance fixed.Int26_6
// xAdvance and yAdvance describe the distance the dot moves when
// laying out the glyph on the X or Y axis.
xAdvance, yAdvance fixed.Int26_6
// xOffset and yOffset describe offsets from the dot that should be
// applied when rendering the glyph.
xOffset, yOffset fixed.Int26_6
@@ -269,9 +270,8 @@ func newShaperImpl(systemFonts bool, collection []FontFace) *shaperImpl {
// in the order in which they are loaded, with the first face being the default.
func (s *shaperImpl) Load(f FontFace) {
desc := opentype.FontToDescription(f.Font)
face := f.Face.Face()
s.fontMap.AddFace(face, fontscan.Location{File: fmt.Sprint(desc)}, desc)
s.addFace(face, f.Font)
s.fontMap.AddFace(f.Face.Face(), fontscan.Location{File: fmt.Sprint(desc)}, desc)
s.addFace(f.Face.Face(), f.Font)
}
func (s *shaperImpl) addFace(f *font.Face, md giofont.Font) {
@@ -437,7 +437,8 @@ func (s *shaperImpl) shapeText(ppem fixed.Int26_6, lc system.Locale, txt []rune)
Height: input.Size,
XBearing: 0,
YBearing: 0,
Advance: input.Size,
XAdvance: input.Size,
YAdvance: input.Size,
XOffset: 0,
YOffset: 0,
ClusterIndex: input.RunStart,
@@ -853,10 +854,11 @@ func toGioGlyphs(in []shaping.Glyph, ppem fixed.Int26_6, faceIdx int) []glyph {
bounds.Max = bounds.Min.Add(fixed.Point26_6{X: g.Width, Y: -g.Height})
out = append(out, glyph{
id: newGlyphID(ppem, faceIdx, g.GlyphID),
clusterIndex: g.TextIndex(),
runeCount: g.RunesCount(),
glyphCount: g.GlyphsCount(),
advance: g.Advance,
clusterIndex: g.ClusterIndex,
runeCount: g.RuneCount,
glyphCount: g.GlyphCount,
xAdvance: g.XAdvance,
yAdvance: g.YAdvance,
xOffset: g.XOffset,
yOffset: g.YOffset,
bounds: bounds,
+7 -3
View File
@@ -259,6 +259,10 @@ func WithCollection(collection []FontFace) ShaperOption {
}
// 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 {
@@ -464,7 +468,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
if rtl {
// Modify the advance prior to computing runOffset to ensure that the
// current glyph's width is subtracted in RTL.
l.advance += g.advance
l.advance += g.xAdvance
}
// runOffset computes how far into the run the dot should be positioned.
runOffset := l.advance
@@ -477,7 +481,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
Y: int32(line.yOffset),
Ascent: line.ascent,
Descent: line.descent,
Advance: g.advance,
Advance: g.xAdvance,
Runes: uint16(g.runeCount),
Offset: fixed.Point26_6{
X: g.xOffset,
@@ -490,7 +494,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
}
l.glyph++
if !rtl {
l.advance += g.advance
l.advance += g.xAdvance
}
endOfRun := l.glyph == len(run.Glyphs)
+2 -2
View File
@@ -450,8 +450,8 @@ func printLinePositioning(t *testing.T, lines []line, glyphs []Glyph) {
for g := start; ; g += inc {
glyph := run.Glyphs[g]
if glyphCursor < len(glyphs) {
t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - glyphs[%2d] flags %s", g, glyph.advance, glyph.runeCount, glyph.glyphCount, glyphCursor, glyphs[glyphCursor].Flags)
t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - n/a", g, glyph.advance, glyph.runeCount, glyph.glyphCount)
t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - glyphs[%2d] flags %s", g, glyph.xAdvance, glyph.runeCount, glyph.glyphCount, glyphCursor, glyphs[glyphCursor].Flags)
t.Logf("glyph %2d, adv %3d, runes %2d, glyphs %d - n/a", g, glyph.xAdvance, glyph.runeCount, glyph.glyphCount)
}
glyphCursor++
if g == end {
+2 -30
View File
@@ -25,7 +25,6 @@ import (
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
)
@@ -108,9 +107,8 @@ type imeState struct {
rng key.Range
caret key.Caret
}
snippet key.Snippet
composition key.Range
start, end int
snippet key.Snippet
start, end int
}
type maskReader struct {
@@ -400,12 +398,9 @@ func (e *Editor) processKey(gtx layout.Context) (EditorEvent, bool) {
case key.FocusEvent:
// Reset IME state.
e.ime.imeState = imeState{}
e.ime.composition = key.Range{Start: -1, End: -1}
if ke.Focus && !e.ReadOnly {
gtx.Execute(key.SoftKeyboardCmd{Show: true})
}
case key.CompositionEvent:
e.ime.composition = key.Range(ke)
case key.Event:
if !gtx.Focused(e) || ke.State != key.Press {
break
@@ -740,7 +735,6 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
if e.Len() > 0 {
e.paintSelection(gtx, selectMaterial)
e.paintText(gtx, textMaterial)
e.paintComposition(gtx, textMaterial)
}
if gtx.Enabled() {
e.paintCaret(gtx, textMaterial)
@@ -765,28 +759,6 @@ func (e *Editor) paintText(gtx layout.Context, material op.CallOp) {
e.text.PaintText(gtx, material)
}
func (e *Editor) paintComposition(gtx layout.Context, material op.CallOp) {
e.initBuffer()
r := e.ime.composition
if r.Start == -1 || r.Start == r.End {
return
}
e.text.regions = e.text.Regions(r.Start, r.End, e.text.regions)
thickness := max(gtx.Dp(unit.Dp(1)), 1)
for _, region := range e.text.regions {
y := region.Bounds.Max.Y - max(region.Baseline/3, thickness)
underline := image.Rect(region.Bounds.Min.X, y, region.Bounds.Max.X, y+thickness)
underline = underline.Intersect(image.Rectangle{Max: e.text.viewSize})
if underline.Empty() {
continue
}
stack := clip.Rect(underline).Push(gtx.Ops)
material.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
stack.Pop()
}
}
// paintCaret paints the text glyphs using the provided material to set the fill material
// of the caret rectangle.
func (e *Editor) paintCaret(gtx layout.Context, material op.CallOp) {
+16 -4
View File
@@ -5,6 +5,7 @@ import (
"image/color"
"gioui.org/f32"
"gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/op"
@@ -85,10 +86,21 @@ func (d DecorationsStyle) layoutDecorations(gtx layout.Context) layout.Dimension
continue
}
cl := d.Decorations.Clickable(a)
dims := Clickable(gtx, cl, func(gtx layout.Context) layout.Dimensions {
system.ActionInputOp(a).Add(gtx.Ops)
paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
return inset.Layout(gtx, w)
dims := cl.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
semantic.Button.Add(gtx.Ops)
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
defer clip.Rect{Max: gtx.Constraints.Min}.Push(gtx.Ops).Pop()
for _, c := range cl.History() {
drawInk(gtx, c)
}
return layout.Dimensions{Size: gtx.Constraints.Min}
},
func(gtx layout.Context) layout.Dimensions {
paint.ColorOp{Color: d.Foreground}.Add(gtx.Ops)
return inset.Layout(gtx, w)
},
)
})
size.X += dims.Size.X
if size.Y < dims.Size.Y {