1package gtkmenu
2
3import (
4 "github.com/bnema/dumber/internal/application/port"
5 "github.com/bnema/puregotk/v4/gdk"
6 "github.com/bnema/puregotk/v4/gobject"
7 "github.com/bnema/puregotk/v4/gtk"
8)
9
10type renderItem struct {
11 item port.MenuItem
12 separator bool
13}
14
15const (
16 contextMenuBoxClass = "context-menu"
17 contextMenuPopoverClass = "context-menu-popover"
18 contextMenuItemClass = "context-menu-item"
19 contextMenuSeparatorClass = "context-menu-separator"
20)
21
22// Renderer shows GTK context menus on the provided anchor widget.
23type Renderer struct {
24 dispatch func(func())
25}
26
27// NewRenderer creates a new Renderer.
28func NewRenderer(dispatch func(func())) *Renderer {
29 return &Renderer{dispatch: dispatch}
30}
31
32func BuildButtons(items []port.MenuItem) []renderItem {
33 renderItems := make([]renderItem, 0, len(items))
34 for _, item := range items {
35 if isSeparator(item) {
36 renderItems = append(renderItems, renderItem{separator: true})
37 continue
38 }
39 renderItems = append(renderItems, renderItem{item: item})
40 }
41 return renderItems
42}
43
44func isSeparator(item port.MenuItem) bool {
45 return item.Action == "" && item.Label == ""
46}
47
48// Show renders a popover menu on the GTK thread.
49func (r *Renderer) Show(
50 items []port.MenuItem,
51 anchor *gtk.Widget,
52 x, y int32,
53 onSelect func(port.MenuItem),
54 onClose func(),
55) {
56 if r == nil {
57 return
58 }
59 run := func() {
60 showMenu(anchor, BuildButtons(items), x, y, onSelect, onClose)
61 }
62 if r.dispatch == nil {
63 run()
64 return
65 }
66 r.dispatch(run)
67}
68
69func showMenu(
70 anchor *gtk.Widget,
71 items []renderItem,
72 x, y int32,
73 onSelect func(port.MenuItem),
74 onClose func(),
75) {
76 if anchor == nil {
77 if onClose != nil {
78 onClose()
79 }
80 return
81 }
82 if !hasRenderableItems(items) {
83 if onClose != nil {
84 onClose()
85 }
86 return
87 }
88
89 box := gtk.NewBox(gtk.OrientationVerticalValue, 0)
90 box.AddCssClass(contextMenuBoxClass)
91
92 selected := false
93 var popover *gtk.Popover
94 parent := ChoosePopoverParent(anchor, anchor.GetParent())
95 popoverHost, overlay := createPopoverHost(parent, x, y)
96
97 for _, item := range items {
98 if item.separator {
99 sep := gtk.NewSeparator(gtk.OrientationHorizontalValue)
100 sep.AddCssClass(contextMenuSeparatorClass)
101 box.Append(&sep.Widget)
102 continue
103 }
104
105 btn := gtk.NewButton()
106 btn.SetLabel(item.item.Label)
107 btn.AddCssClass("flat")
108 btn.AddCssClass(contextMenuItemClass)
109
110 menuItem := item.item
111 clickCb := func(_ gtk.Button) {
112 selected = true
113 if onSelect != nil {
114 onSelect(menuItem)
115 }
116 if popover != nil {
117 popover.Popdown()
118 }
119 }
120 btn.ConnectClicked(&clickCb)
121 box.Append(&btn.Widget)
122 }
123
124 popover = gtk.NewPopover()
125 popover.AddCssClass(contextMenuPopoverClass)
126 popover.SetChild(&box.Widget)
127 attachPopover(popover, popoverHost, parent)
128 popover.SetHasArrow(false)
129 popover.SetAutohide(true)
130
131 if popoverHost == nil {
132 rect := PopoverPointingRect(x, y, func(srcX, srcY float64) (float64, float64, bool) {
133 if anchor == nil || parent == nil || anchor == parent {
134 return srcX, srcY, false
135 }
136 var destX, destY float64
137 if !anchor.TranslateCoordinates(parent, srcX, srcY, &destX, &destY) {
138 return srcX, srcY, false
139 }
140 return destX, destY, true
141 })
142 popover.SetPointingTo(rect)
143 }
144
145 closedCb := func(_ gtk.Popover) {
146 if !selected && onClose != nil {
147 onClose()
148 }
149 cleanupPopover(popover, popoverHost, overlay)
150 }
151 popover.ConnectClosed(&closedCb)
152
153 popupPopover(popover, popoverHost)
154}
155
156// ChoosePopoverParent picks the best parent for a GTK popover.
157func ChoosePopoverParent(anchor, parent *gtk.Widget) *gtk.Widget {
158 if parent != nil {
159 return parent
160 }
161 return anchor
162}
163
164// PopoverPointingRect builds the popover pointing rectangle.
165func PopoverPointingRect(
166 x, y int32,
167 translate func(srcX, srcY float64) (float64, float64, bool),
168) *gdk.Rectangle {
169 destX, destY := float64(x), float64(y)
170 if translate != nil {
171 if tx, ty, ok := translate(destX, destY); ok {
172 destX, destY = tx, ty
173 }
174 }
175 return &gdk.Rectangle{X: int(destX), Y: int(destY), Width: 1, Height: 1}
176}
177
178func createPopoverHost(parent *gtk.Widget, x, y int32) (*gtk.MenuButton, *gtk.Overlay) {
179 overlay := overlayFromWidget(parent)
180 if overlay == nil {
181 return nil, nil
182 }
183 host := gtk.NewMenuButton()
184 host.SetCanFocus(false)
185 host.SetHalign(gtk.AlignStartValue)
186 host.SetValign(gtk.AlignStartValue)
187 host.SetMarginStart(int(x))
188 host.SetMarginTop(int(y))
189 host.SetOpacity(0)
190 host.SetSizeRequest(1, 1)
191 host.SetVisible(true)
192 overlay.AddOverlay(&host.Widget)
193 overlay.SetClipOverlay(&host.Widget, false)
194 overlay.SetMeasureOverlay(&host.Widget, false)
195 return host, overlay
196}
197
198func overlayFromWidget(widget *gtk.Widget) *gtk.Overlay {
199 if widget == nil || widget.GoPointer() == 0 {
200 return nil
201 }
202 if !gobject.TypeCheckInstanceIsAPtr(widget.GoPointer(), gtk.OverlayGLibType()) {
203 return nil
204 }
205 return gtk.OverlayNewFromInternalPtr(widget.GoPointer())
206}
207
208func attachPopover(popover *gtk.Popover, host *gtk.MenuButton, parent *gtk.Widget) {
209 if host != nil {
210 host.SetPopover(popover)
211 return
212 }
213 popover.SetParent(parent)
214}
215
216func cleanupPopover(popover *gtk.Popover, host *gtk.MenuButton, overlay *gtk.Overlay) {
217 if host != nil && overlay != nil {
218 host.SetPopover(nil)
219 overlay.RemoveOverlay(&host.Widget)
220 return
221 }
222 popover.Unparent()
223}
224
225func popupPopover(popover *gtk.Popover, host *gtk.MenuButton) {
226 if host != nil {
227 host.Popup()
228 return
229 }
230 popover.Popup()
231}
232
233func hasRenderableItems(items []renderItem) bool {
234 for _, item := range items {
235 if !item.separator {
236 return true
237 }
238 }
239 return false
240}