File: overlay.go

package info (click to toggle)
golang-github-gcla-gowid 1.4.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 1,456 kB
  • sloc: makefile: 4
file content (335 lines) | stat: -rw-r--r-- 8,920 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// Copyright 2019-2022 Graham Clark. All rights reserved.  Use of this source
// code is governed by the MIT license that can be found in the LICENSE
// file.

// Package overlay is a widget that allows one widget to be overlaid on another.
package overlay

import (
	"fmt"

	"github.com/gcla/gowid"
	"github.com/gcla/gowid/widgets/padding"
	tcell "github.com/gdamore/tcell/v2"
)

//======================================================================

// Utility widget, used only to determine if a user input mouse event is within the bounds of
// a widget. This is different from whether or not a widget handles an event. In the case of overlay,
// an overlaid widget may not handle a mouse event, but if it occludes the widget underneath, that
// lower widget should not accept the mouse event (since it ought to be hidden). So the callback
// is expected to set a flag in the composite overlay widget to say the click was within bounds of the
// upper layer.
//
type MouseCheckerWidget struct {
	gowid.IWidget
	ClickWasInBounds func()
}

func (w *MouseCheckerWidget) SubWidget() gowid.IWidget {
	return w.IWidget
}

func (w *MouseCheckerWidget) SetSubWidget(inner gowid.IWidget, app gowid.IApp) {
	w.IWidget = inner
}

func (w *MouseCheckerWidget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize {
	return w.SubWidget().RenderSize(size, focus, app)
}

func NewMouseChecker(inner gowid.IWidget, clickWasInBounds func()) *MouseCheckerWidget {
	res := &MouseCheckerWidget{inner, clickWasInBounds}
	var _ gowid.ICompositeWidget = res
	return res
}

func (w *MouseCheckerWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool {
	if ev2, ok := ev.(*tcell.EventMouse); ok {
		mx, my := ev2.Position()
		ss := w.RenderSize(size, focus, app)
		if my < ss.BoxRows() && my >= 0 && mx < ss.BoxColumns() && mx >= 0 {
			w.ClickWasInBounds()
		}
	}
	return gowid.UserInputIfSelectable(w.IWidget, ev, size, focus, app)
}

//======================================================================

type IOverlay interface {
	Top() gowid.IWidget
	Bottom() gowid.IWidget
	VAlign() gowid.IVAlignment
	Height() gowid.IWidgetDimension
	HAlign() gowid.IHAlignment
	Width() gowid.IWidgetDimension
	BottomGetsFocus() bool
	TopGetsFocus() bool
	BottomGetsCursor() bool
}

type IIgnoreLowerStyle interface {
	IgnoreLowerStyle() bool
}

type IWidget interface {
	gowid.IWidget
	IOverlay
}

type IWidgetSettable interface {
	IWidget
	SetTop(gowid.IWidget, gowid.IApp)
	SetBottom(gowid.IWidget, gowid.IApp)
	SetVAlign(gowid.IVAlignment, gowid.IApp)
	SetHeight(gowid.IWidgetDimension, gowid.IApp)
	SetHAlign(gowid.IHAlignment, gowid.IApp)
	SetWidth(gowid.IWidgetDimension, gowid.IApp)
}

// Widget overlays one widget on top of another. The bottom widget
// is rendered without the focus at full size. The bottom widget is
// rendered between a horizontal and vertical padding widget set up with
// the sizes provided.
type Widget struct {
	top       gowid.IWidget
	bottom    gowid.IWidget
	vAlign    gowid.IVAlignment
	height    gowid.IWidgetDimension
	hAlign    gowid.IHAlignment
	width     gowid.IWidgetDimension
	opts      Options
	Callbacks *gowid.Callbacks
}

var _ IIgnoreLowerStyle = (*Widget)(nil)

// For callback registration
type Top struct{}
type Bottom struct{}

type Options struct {
	BottomGetsFocus  bool
	TopGetsNoFocus   bool
	BottomGetsCursor bool
	IgnoreLowerStyle bool
}

func New(top, bottom gowid.IWidget,
	valign gowid.IVAlignment, height gowid.IWidgetDimension,
	halign gowid.IHAlignment, width gowid.IWidgetDimension,
	opts ...Options) *Widget {
	var opt Options
	if len(opts) > 0 {
		opt = opts[0]
	}
	res := &Widget{
		top:       top,
		bottom:    bottom,
		vAlign:    valign,
		height:    height,
		hAlign:    halign,
		width:     width,
		opts:      opt,
		Callbacks: gowid.NewCallbacks(),
	}
	var _ gowid.IWidget = res
	var _ IWidgetSettable = res
	return res
}

func (w *Widget) String() string {
	return fmt.Sprintf("overlay[t=%v,b=%v]", w.top, w.bottom)
}

func (w *Widget) BottomGetsCursor() bool {
	return w.opts.BottomGetsCursor
}

func (w *Widget) BottomGetsFocus() bool {
	return w.opts.BottomGetsFocus
}

func (w *Widget) TopGetsFocus() bool {
	return !w.opts.TopGetsNoFocus
}

func (w *Widget) Top() gowid.IWidget {
	return w.top
}

func (w *Widget) SetTop(w2 gowid.IWidget, app gowid.IApp) {
	w.top = w2
	gowid.RunWidgetCallbacks(w.Callbacks, Top{}, app, w)
}

func (w *Widget) Bottom() gowid.IWidget {
	return w.bottom
}

func (w *Widget) SetBottom(w2 gowid.IWidget, app gowid.IApp) {
	w.bottom = w2
	gowid.RunWidgetCallbacks(w.Callbacks, Bottom{}, app, w)
}

func (w *Widget) VAlign() gowid.IVAlignment {
	return w.vAlign
}

func (w *Widget) SetVAlign(valign gowid.IVAlignment, app gowid.IApp) {
	w.vAlign = valign
	gowid.RunWidgetCallbacks(w.Callbacks, gowid.VAlignCB{}, app, w)
}

func (w *Widget) Height() gowid.IWidgetDimension {
	return w.height
}

func (w *Widget) SetHeight(height gowid.IWidgetDimension, app gowid.IApp) {
	w.height = height
	gowid.RunWidgetCallbacks(w.Callbacks, gowid.HeightCB{}, app, w)
}

func (w *Widget) HAlign() gowid.IHAlignment {
	return w.hAlign
}

func (w *Widget) SetHAlign(halign gowid.IHAlignment, app gowid.IApp) {
	w.hAlign = halign
	gowid.RunWidgetCallbacks(w.Callbacks, gowid.HAlignCB{}, app, w)
}

func (w *Widget) Width() gowid.IWidgetDimension {
	return w.width
}

func (w *Widget) SetWidth(width gowid.IWidgetDimension, app gowid.IApp) {
	w.width = width
	gowid.RunWidgetCallbacks(w.Callbacks, gowid.WidthCB{}, app, w)
}

func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox {
	return w.bottom.RenderSize(size, gowid.NotSelected, app)
}

func (w *Widget) Selectable() bool {
	return (w.top != nil && w.top.Selectable()) || w.bottom.Selectable()
}

func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool {
	return UserInput(w, ev, size, focus, app)
}

func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas {
	return Render(w, size, focus, app)
}

func (w *Widget) SubWidget() gowid.IWidget {
	if w.opts.BottomGetsFocus {
		return w.bottom
	} else {
		return w.top
	}
}

func (w *Widget) SetSubWidget(inner gowid.IWidget, app gowid.IApp) {
	if w.opts.BottomGetsFocus {
		w.bottom = inner
	} else {
		w.top = inner
	}
}

func (w *Widget) IgnoreLowerStyle() bool {
	return w.opts.IgnoreLowerStyle
}

//======================================================================

func UserInput(w IOverlay, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool {
	res := false
	notOccluded := true

	if w.Top() == nil {
		res = gowid.UserInputIfSelectable(w.Bottom(), ev, size, focus, app)
	} else {
		top := NewMouseChecker(w.Top(), func() {
			notOccluded = false
		})
		p := padding.New(top, w.VAlign(), w.Height(), w.HAlign(), w.Width())

		res = gowid.UserInputIfSelectable(p, ev, size, focus, app)
		if !res {
			_, ok1 := ev.(*tcell.EventKey)
			_, ok2 := ev.(*tcell.EventMouse)
			_, ok3 := ev.(*tcell.EventPaste)
			if notOccluded && (ok1 || ok2 || ok3) {
				res = gowid.UserInputIfSelectable(w.Bottom(), ev, size, focus, app)
			}
		}
	}
	return res
}

// Merge cells as follows - use upper rune if set, use upper colors if set,
// and use upper style only (don't let any lower run style bleed through)
func mergeAllExceptUpperStyle(lower gowid.Cell, upper gowid.Cell) gowid.Cell {
	res := lower
	if upper.HasRune() {
		res = res.WithRune(upper.Rune())
	}

	ufg, ubg, _ := upper.GetDisplayAttrs()
	if ubg != gowid.ColorNone {
		res = res.WithBackgroundColor(ubg)
	}
	if ufg != gowid.ColorNone {
		res = res.WithForegroundColor(ufg)
	}

	res = res.WithStyle(upper.Style())
	return res
}

type iMergeWithFuncCanvas interface {
	MergeWithFunc(gowid.IMergeCanvas, int, int, gowid.CellMergeFunc, bool)
}

func Render(w IOverlay, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas {
	bfocus := focus.And(w.BottomGetsFocus())
	tfocus := focus.And(w.TopGetsFocus())

	bottomC := w.Bottom().Render(size, bfocus, app)
	if w.Top() == nil {
		return bottomC
	} else {
		bottomC2 := bottomC.Duplicate()
		p2 := padding.New(w.Top(), w.VAlign(), w.Height(), w.HAlign(), w.Width())
		topC := p2.Render(size, tfocus, app)

		var bottomC2mc iMergeWithFuncCanvas
		ign := false
		if wIgn, ok := w.(IIgnoreLowerStyle); ok {
			if gc, ok := bottomC2.(iMergeWithFuncCanvas); ok {
				bottomC2mc = gc
				ign = wIgn.IgnoreLowerStyle()
			}
		}

		if ign {
			bottomC2mc.MergeWithFunc(topC, 0, 0, mergeAllExceptUpperStyle, w.BottomGetsCursor())
		} else {
			bottomC2.MergeUnder(topC, 0, 0, w.BottomGetsCursor())
		}

		return bottomC2
	}
}

//======================================================================
// Local Variables:
// mode: Go
// fill-column: 110
// End: