diff --git a/app/internal/windows/windows.go b/app/internal/windows/windows.go index 5b4bcb5dc..d79a650d6 100644 --- a/app/internal/windows/windows.go +++ b/app/internal/windows/windows.go @@ -113,6 +113,7 @@ const ( HWND_TOPMOST = ^(uint32(1) - 1) // -1 + HTCAPTION = 2 HTCLIENT = 1 HTLEFT = 10 HTRIGHT = 11 diff --git a/app/os_macos.go b/app/os_macos.go index a72fc7769..efb0d48b4 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -232,9 +232,8 @@ type window struct { displayLink *displayLink // redraw is a single entry channel for making sure only one // display link redraw request is in flight. - redraw chan struct{} - cursor pointer.Cursor - lastPress C.CFTypeRef + redraw chan struct{} + cursor pointer.Cursor scale float32 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)) case system.ActionRaise: 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 { @@ -500,6 +493,10 @@ func gio_onText(view, cstr C.CFTypeRef) { //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) { 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 switch cdir { 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 case C.MOUSE_DOWN: typ = pointer.Press - if w.lastPress != 0 { - C.CFRelease(w.lastPress) - w.lastPress = 0 + act, ok := w.w.ActionAt(pos) + if ok && w.config.Mode != Fullscreen { + switch act { + case system.ActionMove: + C.performWindowDragWithEvent(w.window, evt) + return + } } - w.lastPress = C.CFRetain(evt) case C.MOUSE_SCROLL: typ = pointer.Scroll 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 { 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{ Type: typ, Source: pointer.Mouse, Time: t, Buttons: btns, - Position: f32.Point{X: xf, Y: yf}, + Position: pos, Scroll: f32.Point{X: dxf, Y: dyf}, Modifiers: convertMods(mods), }) @@ -768,10 +765,6 @@ func gio_onClose(view C.CFTypeRef) { w.w.Event(ViewEvent{}) deleteView(view) w.w.Event(system.DestroyEvent{}) - if w.lastPress != 0 { - C.CFRelease(w.lastPress) - w.lastPress = 0 - } w.displayLink.Close() C.CFRelease(w.view) C.CFRelease(w.window) diff --git a/app/os_wayland.go b/app/os_wayland.go index 8cb44cacc..9473d98e9 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -879,6 +879,14 @@ func gio_onPointerButton(data unsafe.Pointer, p *C.struct_wl_pointer, serial, t, w.resize(serial, edge) 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: btn = pointer.ButtonSecondary 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 walkActions(actions, func(action system.Action) { switch action { - case system.ActionMove: - w.move() case system.ActionClose: w.dead = true } }) } -func (w *window) move() { - if !w.inCompositor && w.seat != nil { +func (w *window) move(serial C.uint32_t) { + s := w.seat + if !w.inCompositor && s != nil { w.inCompositor = true - s := w.seat - C.xdg_toplevel_move(w.topLvl, s.seat, s.serial) + C.xdg_toplevel_move(w.topLvl, s.seat, serial) } } diff --git a/app/os_windows.go b/app/os_windows.go index 60854a395..6ce3d0e46 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -437,6 +437,11 @@ func (w *window) hitTest(x, y int) uintptr { default: fallthrough 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 case top && left: return windows.HTTOPLEFT diff --git a/app/window.go b/app/window.go index 1e0c067bd..d894c31cd 100644 --- a/app/window.go +++ b/app/window.go @@ -567,6 +567,10 @@ func (c *callbacks) ClickFocus() { 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) { if r.Start > r.End { r.Start, r.End = r.End, r.Start diff --git a/internal/ops/ops.go b/internal/ops/ops.go index fc1812820..275a15ad7 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -75,6 +75,7 @@ const ( TypeSemanticDisabled TypeSnippet TypeSelection + TypeActionInput ) type StackID struct { @@ -158,6 +159,7 @@ const ( TypeSemanticDisabledLen = 2 TypeSnippetLen = 1 + 4 + 4 TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4 + TypeActionInputLen = 1 + 1 ) func (op *ClipOp) Decode(data []byte) { @@ -418,6 +420,7 @@ func (t OpType) Size() int { TypeSemanticDisabledLen, TypeSnippetLen, TypeSelectionLen, + TypeActionInputLen, }[t-firstOpIndex] } diff --git a/io/router/pointer.go b/io/router/pointer.go index 8c5181345..a73860499 100644 --- a/io/router/pointer.go +++ b/io/router/pointer.go @@ -13,6 +13,7 @@ import ( "gioui.org/io/key" "gioui.org/io/pointer" "gioui.org/io/semantic" + "gioui.org/io/system" "gioui.org/io/transfer" ) @@ -97,6 +98,7 @@ type areaNode struct { id SemanticID content semanticContent } + action system.Action } 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) { areaID := c.currentArea() area := &c.q.areas[areaID] @@ -424,6 +432,19 @@ 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 + } + area := q.areas[n.area] + return area.action, area.action != 0 + } + return 0, false +} + func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) { q.assignSemIDs() for i := len(q.hitTree) - 1; i >= 0; i-- { diff --git a/io/router/router.go b/io/router/router.go index 4271cbb47..0b8960e73 100644 --- a/io/router/router.go +++ b/io/router/router.go @@ -27,6 +27,7 @@ import ( "gioui.org/io/pointer" "gioui.org/io/profile" "gioui.org/io/semantic" + "gioui.org/io/system" "gioui.org/io/transfer" "gioui.org/op" ) @@ -276,6 +277,10 @@ func min(p1, p2 image.Point) image.Point { return m } +func (q *Router) ActionAt(p f32.Point) (system.Action, bool) { + return q.pointer.queue.ActionAt(p) +} + func (q *Router) ClickFocus() { focus := q.key.queue.focus if focus == nil { @@ -444,6 +449,9 @@ func (q *Router) collect() { Data: encOp.Refs[2].(io.ReadCloser), } pc.offerOp(op, &q.handlers) + case ops.TypeActionInput: + act := system.Action(encOp.Data[1]) + pc.actionInputOp(act) // Key ops. case ops.TypeKeyFocus: diff --git a/io/system/decoration.go b/io/system/decoration.go index 245e14e18..c9e01660f 100644 --- a/io/system/decoration.go +++ b/io/system/decoration.go @@ -2,8 +2,17 @@ package system import ( "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. type Action uint @@ -31,6 +40,12 @@ const ( 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 { var buf strings.Builder for b := Action(1); a != 0; b <<= 1 { diff --git a/widget/decorations.go b/widget/decorations.go index 8a7de2268..384578253 100644 --- a/widget/decorations.go +++ b/widget/decorations.go @@ -12,7 +12,6 @@ import ( // Decorations handles the states of window decorations. type Decorations struct { - move gesture.Drag clicks []Clickable resize [8]struct { gesture.Hover @@ -25,13 +24,8 @@ type Decorations struct { // LayoutMove lays out the widget that makes a window movable. func (d *Decorations) LayoutMove(gtx layout.Context, w layout.Widget) layout.Dimensions { dims := w(gtx) - d.move.Events(gtx.Metric, gtx, gesture.Both) - st := clip.Rect{Max: dims.Size}.Push(gtx.Ops) - d.move.Add(gtx.Ops) - if d.move.Pressed() { - d.actions |= system.ActionMove - } - st.Pop() + defer clip.Rect{Max: dims.Size}.Push(gtx.Ops).Pop() + system.ActionInputOp(system.ActionMove).Add(gtx.Ops) return dims }