button.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package widget
4
5 import (
6 "image"
7 "time"
8
9 "github.com/p9c/p9/pkg/gel/gio/f32"
10 "github.com/p9c/p9/pkg/gel/gio/gesture"
11 "github.com/p9c/p9/pkg/gel/gio/io/key"
12 "github.com/p9c/p9/pkg/gel/gio/io/pointer"
13 "github.com/p9c/p9/pkg/gel/gio/layout"
14 "github.com/p9c/p9/pkg/gel/gio/op"
15 )
16
17 // Clickable represents a clickable area.
18 type Clickable struct {
19 click gesture.Click
20 clicks []Click
21 // prevClicks is the index into clicks that marks the clicks
22 // from the most recent Layout call. prevClicks is used to keep
23 // clicks bounded.
24 prevClicks int
25 history []Press
26 }
27
28 // Click represents a click.
29 type Click struct {
30 Modifiers key.Modifiers
31 NumClicks int
32 }
33
34 // Press represents a past pointer press.
35 type Press struct {
36 // Position of the press.
37 Position f32.Point
38 // Start is when the press began.
39 Start time.Time
40 // End is when the press was ended by a release or cancel.
41 // A zero End means it hasn't ended yet.
42 End time.Time
43 // Cancelled is true for cancelled presses.
44 Cancelled bool
45 }
46
47 // Click executes a simple programmatic click
48 func (b *Clickable) Click() {
49 b.clicks = append(b.clicks, Click{
50 Modifiers: 0,
51 NumClicks: 1,
52 })
53 }
54
55 // Clicked reports whether there are pending clicks as would be
56 // reported by Clicks. If so, Clicked removes the earliest click.
57 func (b *Clickable) Clicked() bool {
58 if len(b.clicks) == 0 {
59 return false
60 }
61 n := copy(b.clicks, b.clicks[1:])
62 b.clicks = b.clicks[:n]
63 if b.prevClicks > 0 {
64 b.prevClicks--
65 }
66 return true
67 }
68
69 // Hovered returns whether pointer is over the element.
70 func (b *Clickable) Hovered() bool {
71 return b.click.Hovered()
72 }
73
74 // Pressed returns whether pointer is pressing the element.
75 func (b *Clickable) Pressed() bool {
76 return b.click.Pressed()
77 }
78
79 // Clicks returns and clear the clicks since the last call to Clicks.
80 func (b *Clickable) Clicks() []Click {
81 clicks := b.clicks
82 b.clicks = nil
83 b.prevClicks = 0
84 return clicks
85 }
86
87 // History is the past pointer presses useful for drawing markers.
88 // History is retained for a short duration (about a second).
89 func (b *Clickable) History() []Press {
90 return b.history
91 }
92
93 // Layout and update the button state
94 func (b *Clickable) Layout(gtx layout.Context) layout.Dimensions {
95 b.update(gtx)
96 stack := op.Save(gtx.Ops)
97 pointer.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Add(gtx.Ops)
98 b.click.Add(gtx.Ops)
99 stack.Load()
100 for len(b.history) > 0 {
101 c := b.history[0]
102 if c.End.IsZero() || gtx.Now.Sub(c.End) < 1*time.Second {
103 break
104 }
105 n := copy(b.history, b.history[1:])
106 b.history = b.history[:n]
107 }
108 return layout.Dimensions{Size: gtx.Constraints.Min}
109 }
110
111 // update the button state by processing events.
112 func (b *Clickable) update(gtx layout.Context) {
113 // Flush clicks from before the last update.
114 n := copy(b.clicks, b.clicks[b.prevClicks:])
115 b.clicks = b.clicks[:n]
116 b.prevClicks = n
117
118 for _, e := range b.click.Events(gtx) {
119 switch e.Type {
120 case gesture.TypeClick:
121 b.clicks = append(b.clicks, Click{
122 Modifiers: e.Modifiers,
123 NumClicks: e.NumClicks,
124 })
125 if l := len(b.history); l > 0 {
126 b.history[l-1].End = gtx.Now
127 }
128 case gesture.TypeCancel:
129 for i := range b.history {
130 b.history[i].Cancelled = true
131 if b.history[i].End.IsZero() {
132 b.history[i].End = gtx.Now
133 }
134 }
135 case gesture.TypePress:
136 b.history = append(b.history, Press{
137 Position: e.Position,
138 Start: gtx.Now,
139 })
140 }
141 }
142 }
143