io/system: add ActionInputOp to register window move gesture areas

The app.Window.Perform(ActionMove) is the wrong abstraction for
initiating a move gesture: Windows needs to know the move gesture
area at pointer move, and macOS needs to know the pointer button
down event that triggers the move gesture. This change replaces
Perform(ActionMove) with a new system.ActionInputOp that marks an
area movable.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-06-25 19:03:50 +02:00
parent b53cdfef8d
commit 3f38e67ce0
10 changed files with 85 additions and 35 deletions
+1
View File
@@ -113,6 +113,7 @@ const (
HWND_TOPMOST = ^(uint32(1) - 1) // -1 HWND_TOPMOST = ^(uint32(1) - 1) // -1
HTCAPTION = 2
HTCLIENT = 1 HTCLIENT = 1
HTLEFT = 10 HTLEFT = 10
HTRIGHT = 11 HTRIGHT = 11
+14 -21
View File
@@ -232,9 +232,8 @@ type window struct {
displayLink *displayLink displayLink *displayLink
// redraw is a single entry channel for making sure only one // redraw is a single entry channel for making sure only one
// display link redraw request is in flight. // display link redraw request is in flight.
redraw chan struct{} redraw chan struct{}
cursor pointer.Cursor cursor pointer.Cursor
lastPress C.CFTypeRef
scale float32 scale float32
config Config config Config
@@ -413,12 +412,6 @@ func (w *window) Perform(acts system.Action) {
C.setScreenFrame(w.window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y)) C.setScreenFrame(w.window, C.CGFloat(x), C.CGFloat(y), C.CGFloat(sz.X), C.CGFloat(sz.Y))
case system.ActionRaise: case system.ActionRaise:
C.raiseWindow(w.window) C.raiseWindow(w.window)
case system.ActionMove:
if w.lastPress != 0 {
C.performWindowDragWithEvent(w.window, w.lastPress)
C.CFRelease(w.lastPress)
w.lastPress = 0
}
} }
}) })
if acts&system.ActionClose != 0 { if acts&system.ActionClose != 0 {
@@ -500,6 +493,10 @@ func gio_onText(view, cstr C.CFTypeRef) {
//export gio_onMouse //export gio_onMouse
func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) { func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, x, y, dx, dy C.CGFloat, ti C.double, mods C.NSUInteger) {
w := mustView(view) w := mustView(view)
t := time.Duration(float64(ti)*float64(time.Second) + .5)
xf, yf := float32(x)*w.scale, float32(y)*w.scale
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
pos := f32.Point{X: xf, Y: yf}
var typ pointer.Type var typ pointer.Type
switch cdir { switch cdir {
case C.MOUSE_MOVE: case C.MOUSE_MOVE:
@@ -508,11 +505,14 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, x, y, dx
typ = pointer.Release typ = pointer.Release
case C.MOUSE_DOWN: case C.MOUSE_DOWN:
typ = pointer.Press typ = pointer.Press
if w.lastPress != 0 { act, ok := w.w.ActionAt(pos)
C.CFRelease(w.lastPress) if ok && w.config.Mode != Fullscreen {
w.lastPress = 0 switch act {
case system.ActionMove:
C.performWindowDragWithEvent(w.window, evt)
return
}
} }
w.lastPress = C.CFRetain(evt)
case C.MOUSE_SCROLL: case C.MOUSE_SCROLL:
typ = pointer.Scroll typ = pointer.Scroll
default: default:
@@ -528,15 +528,12 @@ func gio_onMouse(view, evt C.CFTypeRef, cdir C.int, cbtns C.NSUInteger, x, y, dx
if cbtns&(1<<2) != 0 { if cbtns&(1<<2) != 0 {
btns |= pointer.ButtonTertiary btns |= pointer.ButtonTertiary
} }
t := time.Duration(float64(ti)*float64(time.Second) + .5)
xf, yf := float32(x)*w.scale, float32(y)*w.scale
dxf, dyf := float32(dx)*w.scale, float32(dy)*w.scale
w.w.Event(pointer.Event{ w.w.Event(pointer.Event{
Type: typ, Type: typ,
Source: pointer.Mouse, Source: pointer.Mouse,
Time: t, Time: t,
Buttons: btns, Buttons: btns,
Position: f32.Point{X: xf, Y: yf}, Position: pos,
Scroll: f32.Point{X: dxf, Y: dyf}, Scroll: f32.Point{X: dxf, Y: dyf},
Modifiers: convertMods(mods), Modifiers: convertMods(mods),
}) })
@@ -768,10 +765,6 @@ func gio_onClose(view C.CFTypeRef) {
w.w.Event(ViewEvent{}) w.w.Event(ViewEvent{})
deleteView(view) deleteView(view)
w.w.Event(system.DestroyEvent{}) w.w.Event(system.DestroyEvent{})
if w.lastPress != 0 {
C.CFRelease(w.lastPress)
w.lastPress = 0
}
w.displayLink.Close() w.displayLink.Close()
C.CFRelease(w.view) C.CFRelease(w.view)
C.CFRelease(w.window) C.CFRelease(w.window)
+12 -6
View File
@@ -879,6 +879,14 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t,
w.resize(serial, edge) w.resize(serial, edge)
return return
} }
act, ok := w.w.ActionAt(w.lastPos)
if ok && w.config.Mode == Windowed {
switch act {
case system.ActionMove:
w.move(serial)
return
}
}
case BTN_RIGHT: case BTN_RIGHT:
btn = pointer.ButtonSecondary btn = pointer.ButtonSecondary
case BTN_MIDDLE: case BTN_MIDDLE:
@@ -1073,19 +1081,17 @@ func (w *window) Perform(actions system.Action) {
// https://wayland.app/protocols/xdg-shell#xdg_toplevel:request:set_minimized // https://wayland.app/protocols/xdg-shell#xdg_toplevel:request:set_minimized
walkActions(actions, func(action system.Action) { walkActions(actions, func(action system.Action) {
switch action { switch action {
case system.ActionMove:
w.move()
case system.ActionClose: case system.ActionClose:
w.dead = true w.dead = true
} }
}) })
} }
func (w *window) move() { func (w *window) move(serial C.uint32_t) {
if !w.inCompositor && w.seat != nil { s := w.seat
if !w.inCompositor && s != nil {
w.inCompositor = true w.inCompositor = true
s := w.seat C.xdg_toplevel_move(w.topLvl, s.seat, serial)
C.xdg_toplevel_move(w.topLvl, s.seat, s.serial)
} }
} }
+5
View File
@@ -437,6 +437,11 @@ func (w *window) hitTest(x, y int) uintptr {
default: default:
fallthrough fallthrough
case !top && !bottom && !left && !right: case !top && !bottom && !left && !right:
p := f32.Pt(float32(x), float32(y))
switch a, _ := w.w.ActionAt(p); a {
case system.ActionMove:
return windows.HTCAPTION
}
return windows.HTCLIENT return windows.HTCLIENT
case top && left: case top && left:
return windows.HTTOPLEFT return windows.HTTOPLEFT
+4
View File
@@ -567,6 +567,10 @@ func (c *callbacks) ClickFocus() {
c.w.updateAnimation(c.d) c.w.updateAnimation(c.d)
} }
func (c *callbacks) ActionAt(p f32.Point) (system.Action, bool) {
return c.w.queue.q.ActionAt(p)
}
func (e *editorState) Replace(r key.Range, text string) { func (e *editorState) Replace(r key.Range, text string) {
if r.Start > r.End { if r.Start > r.End {
r.Start, r.End = r.End, r.Start r.Start, r.End = r.End, r.Start
+3
View File
@@ -75,6 +75,7 @@ const (
TypeSemanticDisabled TypeSemanticDisabled
TypeSnippet TypeSnippet
TypeSelection TypeSelection
TypeActionInput
) )
type StackID struct { type StackID struct {
@@ -158,6 +159,7 @@ const (
TypeSemanticDisabledLen = 2 TypeSemanticDisabledLen = 2
TypeSnippetLen = 1 + 4 + 4 TypeSnippetLen = 1 + 4 + 4
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4 TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
TypeActionInputLen = 1 + 1
) )
func (op *ClipOp) Decode(data []byte) { func (op *ClipOp) Decode(data []byte) {
@@ -418,6 +420,7 @@ func (t OpType) Size() int {
TypeSemanticDisabledLen, TypeSemanticDisabledLen,
TypeSnippetLen, TypeSnippetLen,
TypeSelectionLen, TypeSelectionLen,
TypeActionInputLen,
}[t-firstOpIndex] }[t-firstOpIndex]
} }
+21
View File
@@ -13,6 +13,7 @@ import (
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/io/transfer" "gioui.org/io/transfer"
) )
@@ -97,6 +98,7 @@ type areaNode struct {
id SemanticID id SemanticID
content semanticContent content semanticContent
} }
action system.Action
} }
type areaKind uint8 type areaKind uint8
@@ -256,6 +258,12 @@ func (c *pointerCollector) keyInputOp(op key.InputOp) {
}) })
} }
func (c *pointerCollector) actionInputOp(act system.Action) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.action = act
}
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) { func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
areaID := c.currentArea() areaID := c.currentArea()
area := &c.q.areas[areaID] area := &c.q.areas[areaID]
@@ -424,6 +432,19 @@ func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
return id.id 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
}
area := q.areas[n.area]
return area.action, area.action != 0
}
return 0, false
}
func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) { func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) {
q.assignSemIDs() q.assignSemIDs()
for i := len(q.hitTree) - 1; i >= 0; i-- { for i := len(q.hitTree) - 1; i >= 0; i-- {
+8
View File
@@ -27,6 +27,7 @@ import (
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/profile" "gioui.org/io/profile"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/io/system"
"gioui.org/io/transfer" "gioui.org/io/transfer"
"gioui.org/op" "gioui.org/op"
) )
@@ -276,6 +277,10 @@ func min(p1, p2 image.Point) image.Point {
return m return m
} }
func (q *Router) ActionAt(p f32.Point) (system.Action, bool) {
return q.pointer.queue.ActionAt(p)
}
func (q *Router) ClickFocus() { func (q *Router) ClickFocus() {
focus := q.key.queue.focus focus := q.key.queue.focus
if focus == nil { if focus == nil {
@@ -444,6 +449,9 @@ func (q *Router) collect() {
Data: encOp.Refs[2].(io.ReadCloser), Data: encOp.Refs[2].(io.ReadCloser),
} }
pc.offerOp(op, &q.handlers) pc.offerOp(op, &q.handlers)
case ops.TypeActionInput:
act := system.Action(encOp.Data[1])
pc.actionInputOp(act)
// Key ops. // Key ops.
case ops.TypeKeyFocus: case ops.TypeKeyFocus:
+15
View File
@@ -2,8 +2,17 @@ package system
import ( import (
"strings" "strings"
"gioui.org/internal/ops"
"gioui.org/op"
) )
// ActionAreaOp makes the current clip area available for
// system gestures.
//
// Note: only ActionMove is supported.
type ActionInputOp Action
// Action is a set of window decoration actions. // Action is a set of window decoration actions.
type Action uint type Action uint
@@ -31,6 +40,12 @@ const (
ActionMove ActionMove
) )
func (op ActionInputOp) Add(o *op.Ops) {
data := ops.Write(&o.Internal, ops.TypeActionInputLen)
data[0] = byte(ops.TypeActionInput)
data[1] = byte(op)
}
func (a Action) String() string { func (a Action) String() string {
var buf strings.Builder var buf strings.Builder
for b := Action(1); a != 0; b <<= 1 { for b := Action(1); a != 0; b <<= 1 {
+2 -8
View File
@@ -12,7 +12,6 @@ import (
// Decorations handles the states of window decorations. // Decorations handles the states of window decorations.
type Decorations struct { type Decorations struct {
move gesture.Drag
clicks []Clickable clicks []Clickable
resize [8]struct { resize [8]struct {
gesture.Hover gesture.Hover
@@ -25,13 +24,8 @@ type Decorations struct {
// LayoutMove lays out the widget that makes a window movable. // LayoutMove lays out the widget that makes a window movable.
func (d *Decorations) LayoutMove(gtx layout.Context, w layout.Widget) layout.Dimensions { func (d *Decorations) LayoutMove(gtx layout.Context, w layout.Widget) layout.Dimensions {
dims := w(gtx) dims := w(gtx)
d.move.Events(gtx.Metric, gtx, gesture.Both) defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop()
st := clip.Rect{Max: dims.Size}.Push(gtx.Ops) system.ActionInputOp(system.ActionMove).Add(gtx.Ops)
d.move.Add(gtx.Ops)
if d.move.Pressed() {
d.actions |= system.ActionMove
}
st.Pop()
return dims return dims
} }