~whereswaldon/gio-x

6db76265c4e147ad43305e9440cf36e21cd7b482 — Chris Waldon 2 months ago ab05db3
fix: prevent multiple simultaneous menus

This fixes a bug in which a ContextArea could display its
contextual widget twice if the user right-clicked quickly
while the area was open.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
1 files changed, 78 insertions(+), 29 deletions(-)

M component/context-area.go
M component/context-area.go => component/context-area.go +78 -29
@@ 23,13 23,11 @@ type ContextArea struct {
// Layout renders the context area and -- if the area is activated by an
// appropriate gesture -- also the provided widget overlaid using an op.DeferOp.
func (r *ContextArea) Layout(gtx C, w layout.Widget) D {
	pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
	pointer.PassOp{Pass: true}.Add(gtx.Ops)
	pointer.InputOp{
		Tag:   r,
		Grab:  false,
		Types: pointer.Press | pointer.Release,
	}.Add(gtx.Ops)
	suppressionTag := &r.active
	dismissTag := &r.dims

	startedActive := r.active
	// Summon the contextual widget if the area recieved a secondary click.
	for _, e := range gtx.Events(r) {
		e, ok := e.(pointer.Event)
		if !ok {


@@ 44,18 42,24 @@ func (r *ContextArea) Layout(gtx C, w layout.Widget) D {
				}
			}
		}
		if e.Buttons.Contain(pointer.ButtonSecondary) {
		if e.Buttons.Contain(pointer.ButtonSecondary) && e.Type == pointer.Press {
			r.active = true
			r.position = e.Position
		}
	}
	dims := D{Size: gtx.Constraints.Min}

	if !r.active {
		return dims
	// Dismiss the contextual widget if the user clicked outside of it.
	for _, e := range gtx.Events(suppressionTag) {
		e, ok := e.(pointer.Event)
		if !ok {
			continue
		}
		if e.Type == pointer.Press {
			r.Dismiss()
		}
	}

	for _, e := range gtx.Events(&r.active) {
	// Dismiss the contextual widget if the user released a click within it.
	for _, e := range gtx.Events(dismissTag) {
		e, ok := e.(pointer.Event)
		if !ok {
			continue


@@ 65,29 69,74 @@ func (r *ContextArea) Layout(gtx C, w layout.Widget) D {
		}
	}

	defer op.Save(gtx.Ops).Load()
	macro := op.Record(gtx.Ops)
	r.dims = w(gtx)
	call := macro.Stop()
	dims := D{Size: gtx.Constraints.Min}

	if int(r.position.X)+r.dims.Size.X > gtx.Constraints.Max.X {
		r.position.X = float32(gtx.Constraints.Max.X - r.dims.Size.X)
	var contextual op.CallOp
	if r.active || startedActive {
		// Render if the layout started as active to ensure that widgets
		// within the contextual content get to update their state in reponse
		// to the event that dismissed the contextual widget.
		contextual = func() op.CallOp {
			defer op.Save(gtx.Ops).Load()
			macro := op.Record(gtx.Ops)
			r.dims = w(gtx)
			return macro.Stop()
		}()
	}
	if int(r.position.Y)+r.dims.Size.Y > gtx.Constraints.Max.Y {
		r.position.Y = float32(gtx.Constraints.Max.Y - r.dims.Size.Y)

	if r.active {
		if int(r.position.X)+r.dims.Size.X > gtx.Constraints.Max.X {
			r.position.X = float32(gtx.Constraints.Max.X - r.dims.Size.X)
		}
		if int(r.position.Y)+r.dims.Size.Y > gtx.Constraints.Max.Y {
			r.position.Y = float32(gtx.Constraints.Max.Y - r.dims.Size.Y)
		}
		// Lay out a transparent scrim to block input to things beneath the
		// contextual widget.
		suppressionScrim := func() op.CallOp {
			defer op.Save(gtx.Ops).Load()
			macro2 := op.Record(gtx.Ops)
			pointer.PassOp{Pass: false}.Add(gtx.Ops)
			pointer.Rect(image.Rectangle{Min: image.Point{-1e6, -1e6}, Max: image.Point{1e6, 1e6}}).Add(gtx.Ops)
			pointer.InputOp{
				Tag:   suppressionTag,
				Grab:  false,
				Types: pointer.Press,
			}.Add(gtx.Ops)
			return macro2.Stop()
		}()
		op.Defer(gtx.Ops, suppressionScrim)

		// Lay out the contextual widget itself.
		macro := op.Record(gtx.Ops)
		op.Offset(r.position).Add(gtx.Ops)
		contextual.Add(gtx.Ops)

		// Lay out a scrim on top of the contextual widget to detect
		// completed interactions with it (that should dismiss it).
		saved := op.Save(gtx.Ops)
		pointer.PassOp{Pass: true}.Add(gtx.Ops)
		pointer.Rect(image.Rectangle{Max: r.dims.Size}).Add(gtx.Ops)
		pointer.InputOp{
			Tag:   dismissTag,
			Grab:  false,
			Types: pointer.Release,
		}.Add(gtx.Ops)

		saved.Load()
		contextual = macro.Stop()
		op.Defer(gtx.Ops, contextual)
	}
	macro2 := op.Record(gtx.Ops)
	op.Offset(r.position).Add(gtx.Ops)
	call.Add(gtx.Ops)

	// Capture pointer events in the contextual area.
	pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
	pointer.PassOp{Pass: true}.Add(gtx.Ops)
	pointer.Rect(image.Rectangle{Min: image.Point{-1e6, -1e6}, Max: image.Point{1e6, 1e6}}).Add(gtx.Ops)
	pointer.InputOp{
		Tag:   &r.active,
		Tag:   r,
		Grab:  false,
		Types: pointer.Release,
		Types: pointer.Press | pointer.Release,
	}.Add(gtx.Ops)
	call2 := macro2.Stop()
	op.Defer(gtx.Ops, call2)

	return dims
}