forked from joejulian/gio
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dec57aea1c | |||
| e2e2c1a046 | |||
| e8c1e1ba11 | |||
| b1cadbdd76 | |||
| 451b7d3a74 | |||
| e49c5b02c7 |
@@ -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.Face{Font: f.face}
|
||||
return fontapi.NewFace(f.face)
|
||||
}
|
||||
|
||||
// FontFace returns a text.Font with populated font metadata for the
|
||||
|
||||
+3
-12
@@ -61,12 +61,8 @@ func (h *Hover) Update(q input.Source) bool {
|
||||
h.entered = false
|
||||
}
|
||||
case pointer.Enter:
|
||||
if !h.entered {
|
||||
h.pid = e.PointerID
|
||||
}
|
||||
if h.pid == e.PointerID {
|
||||
h.entered = true
|
||||
}
|
||||
h.pid = e.PointerID
|
||||
h.entered = true
|
||||
}
|
||||
}
|
||||
return h.entered
|
||||
@@ -222,12 +218,7 @@ func (c *Click) Update(q input.Source) (ClickEvent, bool) {
|
||||
if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonPrimary {
|
||||
break
|
||||
}
|
||||
if !c.hovered {
|
||||
c.pid = e.PointerID
|
||||
}
|
||||
if c.pid != e.PointerID {
|
||||
break
|
||||
}
|
||||
c.pid = e.PointerID
|
||||
c.pressed = true
|
||||
if e.Time-c.clickedAt < doubleClickDuration {
|
||||
c.clicks++
|
||||
|
||||
@@ -100,6 +100,78 @@ 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,
|
||||
|
||||
@@ -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.0
|
||||
github.com/go-text/typesetting v0.3.4
|
||||
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
|
||||
|
||||
@@ -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.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=
|
||||
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=
|
||||
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=
|
||||
|
||||
+16
-3
@@ -739,6 +739,10 @@ 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]
|
||||
|
||||
@@ -761,9 +765,6 @@ func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState,
|
||||
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")
|
||||
}
|
||||
@@ -780,6 +781,18 @@ 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
|
||||
|
||||
@@ -1345,3 +1345,40 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ 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.
|
||||
// Release. Populated for Press, Release, Move, Drag,
|
||||
// Enter, Leave, and Cancel; Scroll events are not
|
||||
// bound to a tracked pointer and leave it zero.
|
||||
PointerID ID
|
||||
// Priority is the priority of the receiving handler
|
||||
// for this event.
|
||||
|
||||
+12
-14
@@ -103,8 +103,7 @@ func (l *line) insertTrailingSyntheticNewline(newLineClusterIdx int) {
|
||||
clusterIndex: newLineClusterIdx,
|
||||
glyphCount: 0,
|
||||
runeCount: 1,
|
||||
xAdvance: 0,
|
||||
yAdvance: 0,
|
||||
advance: 0,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
}
|
||||
@@ -160,9 +159,9 @@ type glyph struct {
|
||||
// runeCount is the quantity of runes in the source text that this glyph
|
||||
// corresponds to.
|
||||
runeCount int
|
||||
// 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
|
||||
// advance is the distance the dot moves when laying out the glyph along
|
||||
// the run's primary axis.
|
||||
advance fixed.Int26_6
|
||||
// xOffset and yOffset describe offsets from the dot that should be
|
||||
// applied when rendering the glyph.
|
||||
xOffset, yOffset fixed.Int26_6
|
||||
@@ -270,8 +269,9 @@ 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)
|
||||
s.fontMap.AddFace(f.Face.Face(), fontscan.Location{File: fmt.Sprint(desc)}, desc)
|
||||
s.addFace(f.Face.Face(), f.Font)
|
||||
face := f.Face.Face()
|
||||
s.fontMap.AddFace(face, fontscan.Location{File: fmt.Sprint(desc)}, desc)
|
||||
s.addFace(face, f.Font)
|
||||
}
|
||||
|
||||
func (s *shaperImpl) addFace(f *font.Face, md giofont.Font) {
|
||||
@@ -437,8 +437,7 @@ func (s *shaperImpl) shapeText(ppem fixed.Int26_6, lc system.Locale, txt []rune)
|
||||
Height: input.Size,
|
||||
XBearing: 0,
|
||||
YBearing: 0,
|
||||
XAdvance: input.Size,
|
||||
YAdvance: input.Size,
|
||||
Advance: input.Size,
|
||||
XOffset: 0,
|
||||
YOffset: 0,
|
||||
ClusterIndex: input.RunStart,
|
||||
@@ -854,11 +853,10 @@ 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.ClusterIndex,
|
||||
runeCount: g.RuneCount,
|
||||
glyphCount: g.GlyphCount,
|
||||
xAdvance: g.XAdvance,
|
||||
yAdvance: g.YAdvance,
|
||||
clusterIndex: g.TextIndex(),
|
||||
runeCount: g.RunesCount(),
|
||||
glyphCount: g.GlyphsCount(),
|
||||
advance: g.Advance,
|
||||
xOffset: g.XOffset,
|
||||
yOffset: g.YOffset,
|
||||
bounds: bounds,
|
||||
|
||||
+3
-7
@@ -259,10 +259,6 @@ 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 {
|
||||
@@ -468,7 +464,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.xAdvance
|
||||
l.advance += g.advance
|
||||
}
|
||||
// runOffset computes how far into the run the dot should be positioned.
|
||||
runOffset := l.advance
|
||||
@@ -481,7 +477,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
|
||||
Y: int32(line.yOffset),
|
||||
Ascent: line.ascent,
|
||||
Descent: line.descent,
|
||||
Advance: g.xAdvance,
|
||||
Advance: g.advance,
|
||||
Runes: uint16(g.runeCount),
|
||||
Offset: fixed.Point26_6{
|
||||
X: g.xOffset,
|
||||
@@ -494,7 +490,7 @@ func (l *Shaper) NextGlyph() (_ Glyph, ok bool) {
|
||||
}
|
||||
l.glyph++
|
||||
if !rtl {
|
||||
l.advance += g.xAdvance
|
||||
l.advance += g.advance
|
||||
}
|
||||
|
||||
endOfRun := l.glyph == len(run.Glyphs)
|
||||
|
||||
+2
-2
@@ -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.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)
|
||||
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)
|
||||
}
|
||||
glyphCursor++
|
||||
if g == end {
|
||||
|
||||
Reference in New Issue
Block a user