Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 42 additions & 8 deletions app/event_bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ func attachEventBridge(es gpucontext.EventSource, w *Window) {
// (the platform's OnScroll callback doesn't provide mouse coordinates).
var lastMousePos geometry.Point

// Track the keyboard modifiers so mouse and wheel events can carry them.
// The platform's mouse callbacks report only a button and a position, so
// without this every MouseEvent is built with ModNone and ⌥click, ⇧click,
// ⌃click and shift-extend-selection are simply not expressible — an app
// either does without them or reimplements this tracking itself.
var mods event.Modifiers

es.OnMouseMove(func(x, y float64) {
pos := geometry.Pt(float32(x), float32(y))
lastMousePos = pos
Expand All @@ -33,7 +40,7 @@ func attachEventBridge(es gpucontext.EventSource, w *Window) {
pressedButtons,
pos,
pos, // global position same as local for root dispatch
event.ModNone,
mods,
)
w.HandleEvent(e)
})
Expand All @@ -48,7 +55,7 @@ func attachEventBridge(es gpucontext.EventSource, w *Window) {
pressedButtons,
pos,
pos,
event.ModNone,
mods,
)
w.HandleEvent(e)
})
Expand All @@ -63,14 +70,19 @@ func attachEventBridge(es gpucontext.EventSource, w *Window) {
pressedButtons,
pos,
pos,
event.ModNone,
mods,
)
w.HandleEvent(e)
})

es.OnKeyPress(func(key gpucontext.Key, mods gpucontext.Modifiers) {
es.OnKeyPress(func(key gpucontext.Key, platMods gpucontext.Modifiers) {
uiKey := translateKey(key)
uiMods := translateModifiers(mods)
uiMods := translateModifiers(platMods)
// A key event reports the modifiers held BEFORE it, so pressing Shift
// alone reports no Shift. Fold the key itself in, or holding a modifier
// and clicking — with no other key in between, which is the whole
// gesture — would leave the state empty.
mods = uiMods | modifierForKey(uiKey)
// Rune=0: character input is delivered separately via OnTextInput.
// KeyPress only carries the key code for navigation (arrows, Tab,
// Backspace, etc.) and modifier detection (Ctrl+C, etc.).
Expand All @@ -83,9 +95,11 @@ func attachEventBridge(es gpucontext.EventSource, w *Window) {
w.HandleEvent(e)
})

es.OnKeyRelease(func(key gpucontext.Key, mods gpucontext.Modifiers) {
es.OnKeyRelease(func(key gpucontext.Key, platMods gpucontext.Modifiers) {
uiKey := translateKey(key)
uiMods := translateModifiers(mods)
uiMods := translateModifiers(platMods)
// Releasing a modifier clears it: the reported state still contains it.
mods = uiMods &^ modifierForKey(uiKey)
e := event.NewKeyEvent(
event.KeyRelease,
uiKey,
Expand Down Expand Up @@ -113,7 +127,7 @@ func attachEventBridge(es gpucontext.EventSource, w *Window) {
delta,
lastMousePos,
lastMousePos,
event.ModNone,
mods,
)
w.HandleEvent(e)
})
Expand All @@ -123,6 +137,10 @@ func attachEventBridge(es gpucontext.EventSource, w *Window) {
})

es.OnFocus(func(focused bool) {
// A modifier believed to be held after the window lost focus would turn
// the next ordinary click into a modified one: the release happened
// somewhere else and this window never saw it.
mods = event.ModNone
w.HandleFocusChange(focused)
})

Expand Down Expand Up @@ -447,6 +465,22 @@ func translateKey(key gpucontext.Key) event.Key {
}

// translateModifiers converts gpucontext.Modifiers to event.Modifiers.
// modifierForKey is the modifier bit a key sets while it is held, or ModNone
// for anything that is not a modifier.
func modifierForKey(k event.Key) event.Modifiers {
switch k {
case event.KeyLeftShift, event.KeyRightShift:
return event.ModShift
case event.KeyLeftCtrl, event.KeyRightCtrl:
return event.ModCtrl
case event.KeyLeftAlt, event.KeyRightAlt:
return event.ModAlt
case event.KeyLeftSuper, event.KeyRightSuper:
return event.ModSuper
}
return event.ModNone
}

func translateModifiers(mods gpucontext.Modifiers) event.Modifiers {
var result event.Modifiers
if mods.HasShift() {
Expand Down
97 changes: 97 additions & 0 deletions app/event_bridge_modifiers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package app

import (
"testing"

"github.com/gogpu/gpucontext"
"github.com/gogpu/ui/event"
"github.com/gogpu/ui/geometry"
"github.com/gogpu/ui/state"
"github.com/gogpu/ui/theme"
"github.com/gogpu/ui/widget"
)

// modRecorder captures the modifiers of every mouse event it is handed.
type modRecorder struct {
widget.WidgetBase
got []event.Modifiers
seen int
}

func (w *modRecorder) Layout(_ widget.Context, c geometry.Constraints) geometry.Size {
return geometry.Sz(c.MaxWidth, c.MaxHeight)
}
func (w *modRecorder) Draw(widget.Context, widget.Canvas) {}
func (w *modRecorder) Event(_ widget.Context, e event.Event) bool {
if me, ok := e.(*event.MouseEvent); ok {
w.got = append(w.got, me.Modifiers())
w.seen++
return true
}
return false
}

func bridgeWithRecorder(t *testing.T) (*mockEventSource, *modRecorder) {
t.Helper()
es := &mockEventSource{}
sched := state.NewScheduler(func(_ []widget.Widget) {})
w := newWindow(nil, nil, sched, theme.DefaultLight(), RenderModeHostManaged)
rec := &modRecorder{}
rec.SetVisible(true)
rec.SetEnabled(true)
w.SetRoot(rec)
attachEventBridge(es, w)
return es, rec
}

// A click while a modifier is held has to arrive as a modified click.
//
// The platform's mouse callbacks carry only a button and a position, so the
// bridge has to remember what the keyboard is holding. Without that every mouse
// event is built with ModNone and ⌥click, ⇧click and ⌃click cannot be expressed
// at all — an application either does without them or reimplements this
// tracking on top of the key events itself.
func TestMouseEventsCarryHeldModifiers(t *testing.T) {
es, rec := bridgeWithRecorder(t)

// Alt down, then click. Nothing else is pressed in between: that is the
// gesture, and it is why the key's own modifier bit has to be folded in —
// a key event reports what was held BEFORE it, so this one reports nothing.
es.onKeyPress(gpucontext.KeyLeftAlt, gpucontext.Modifiers(0))
es.onMousePress(gpucontext.MouseButtonLeft, 10, 10)

if rec.seen == 0 {
t.Fatal("the click never reached the widget")
}
if last := rec.got[len(rec.got)-1]; !last.Has(event.ModAlt) {
t.Errorf("click carried %v, want Alt — a modifier held over a click is lost", last)
}
}

// Releasing the modifier stops modifying the clicks that follow.
func TestMouseModifiersClearOnRelease(t *testing.T) {
es, rec := bridgeWithRecorder(t)

es.onKeyPress(gpucontext.KeyLeftAlt, gpucontext.Modifiers(0))
es.onKeyRelease(gpucontext.KeyLeftAlt, gpucontext.Modifiers(0))
es.onMousePress(gpucontext.MouseButtonLeft, 10, 10)

if last := rec.got[len(rec.got)-1]; last.Has(event.ModAlt) {
t.Errorf("click carried %v after Alt was released", last)
}
}

// A modifier released while another window had focus was never seen here, so
// holding it must not survive the focus change — otherwise the next ordinary
// click is silently a modified one.
func TestMouseModifiersClearOnFocusLoss(t *testing.T) {
es, rec := bridgeWithRecorder(t)

es.onKeyPress(gpucontext.KeyLeftAlt, gpucontext.Modifiers(0))
es.onFocus(false)
es.onMousePress(gpucontext.MouseButtonLeft, 10, 10)

if last := rec.got[len(rec.got)-1]; last.Has(event.ModAlt) {
t.Errorf("click carried %v after the window lost focus", last)
}
}
Loading