/
node.go
446 lines (378 loc) · 14.6 KB
/
node.go
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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
// Copyright (c) 2018, The GoKi Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gi
import (
"image"
"log"
"sync"
"github.com/goki/gi/gist"
"github.com/goki/ki/ki"
"github.com/goki/ki/kit"
"github.com/goki/mat32"
)
// Node is the interface for all GoGi nodes (2D and 3D), for accessing as NodeBase
type Node interface {
// nodes are Ki elements -- this comes for free by embedding ki.Node in NodeBase
ki.Ki
// AsGiNode returns a generic gi.NodeBase for our node -- gives generic
// access to all the base-level data structures without requiring
// interface methods.
AsGiNode() *NodeBase
}
// NodeBase is the base struct type for GoGi graphical interface system,
// containing infrastructure for both 2D and 3D scene graph nodes
type NodeBase struct {
ki.Node
Class string `desc:"user-defined class name(s) used primarily for attaching CSS styles to different display elements -- multiple class names can be used to combine properties: use spaces to separate per css standard"`
CSS ki.Props `xml:"css" desc:"cascading style sheet at this level -- these styles apply here and to everything below, until superceded -- use .class and #name Props elements to apply entire styles to given elements, and type for element type"`
CSSAgg ki.Props `copy:"-" json:"-" xml:"-" view:"no-inline" desc:"aggregated css properties from all higher nodes down to me"`
BBox image.Rectangle `copy:"-" json:"-" xml:"-" desc:"raw original 2D bounding box for the object within its parent viewport -- used for computing VpBBox and WinBBox -- this is not updated by Move2D, whereas VpBBox etc are"`
ObjBBox image.Rectangle `copy:"-" json:"-" xml:"-" desc:"full object bbox -- this is BBox + Move2D delta, but NOT intersected with parent's parBBox -- used for computing color gradients or other object-specific geometry computations"`
VpBBox image.Rectangle `copy:"-" json:"-" xml:"-" desc:"2D bounding box for region occupied within immediate parent Viewport object that we render onto -- these are the pixels we draw into, filtered through parent bounding boxes -- used for render Bounds clipping"`
WinBBox image.Rectangle `copy:"-" json:"-" xml:"-" desc:"2D bounding box for region occupied within parent Window object, projected all the way up to that -- these are the coordinates where we receive events, relative to the window"`
BBoxMu sync.RWMutex `view:"-" copy:"-" json:"-" xml:"-" desc:"mutex protecting access to the WinBBox, which is used for event delegation and could also be updated in another thread"`
}
var KiT_NodeBase = kit.Types.AddType(&NodeBase{}, NodeBaseProps)
var NodeBaseProps = ki.Props{
"base-type": true, // excludes type from user selections
"EnumType:Flag": KiT_NodeFlags,
}
func (nb *NodeBase) AsGiNode() *NodeBase {
return nb
}
func (nb *NodeBase) CopyFieldsFrom(frm any) {
// note: not copying ki.Node as it doesn't have any copy fields
fr := frm.(*NodeBase)
nb.Class = fr.Class
nb.CSS.CopyFrom(fr.CSS, true)
}
// NodeFlags define gi node bitflags for tracking common high-frequency GUI
// state, mostly having to do with event processing -- use properties map for
// less frequently used information -- uses ki Flags field (64 bit capacity)
type NodeFlags int
//go:generate stringer -type=NodeFlags
var KiT_NodeFlags = kit.Enums.AddEnumExt(ki.KiT_Flags, NodeFlagsN, kit.BitFlag, nil)
const (
// NoLayout means that this node does not participate in the layout
// process (Size, Layout, Move) -- set by e.g., SVG nodes
NoLayout NodeFlags = NodeFlags(ki.FlagsN) + iota
// EventsConnected: this node has been connected to receive events from
// the window -- to optimize event processing, connections are typically
// only established for visible nodes during render, and disconnected when
// not visible
EventsConnected
// CanFocus: can this node accept focus to receive keyboard input events
// -- set by default for typical nodes that do so, but can be overridden,
// including by the style 'can-focus' property
CanFocus
// HasFocus: does this node currently have the focus for keyboard input
// events? use tab / alt tab and clicking events to update focus -- see
// interface on Window
HasFocus
// FullReRender indicates that a full re-render is required due to nature
// of update event -- otherwise default is local re-render -- used
// internally for nodes to determine what to do on the ReRender step
FullReRender
// ReRenderAnchor: this node has a static size, and repaints its
// background -- any children under it that need to dynamically resize on
// a ReRender (Update) can refer the update up to rerendering this node,
// instead of going further up the tree -- e.g., true of Frame's within a
// SplitView
ReRenderAnchor
// Invisible means that the node has been marked as invisible by a parent
// that has switch-like powers (e.g., layout stacked / tabview or splitter
// panel that has been collapsed). This flag is propagated down to all
// child nodes, and rendering or other interaction / update routines
// should not run when this flag is set (PushBounds does this for most
// cases). However, it IS a good idea to have styling, layout etc all
// take place as normal, so that when the flag is cleared, rendering can
// proceed directly.
Invisible
// Inactive disables interaction with widgets or other nodes (i.e., they
// are read-only) -- they should indicate this inactive state in an
// appropriate way, and each node should interpret events appropriately
// based on this state (select and context menu events should still be
// generated)
Inactive
// Selected indicates that this node has been selected by the user --
// widely supported across different nodes
Selected
// MouseHasEntered indicates that the MouseFocusEvent Enter was previously
// registered on this node
MouseHasEntered
// DNDHasEntered indicates that the DNDFocusEvent Enter was previously
// registered on this node
DNDHasEntered
// NodeDragging indicates this node is currently dragging -- win.Dragging
// set to this node
NodeDragging
// InstaDrag indicates this node should start dragging immediately when
// clicked -- otherwise there is a time-and-distance threshold to the
// start of dragging -- use this for controls that are small and are
// primarily about dragging (e.g., the Splitter handle)
InstaDrag
// can extend node flags from here
NodeFlagsN
)
// HasNoLayout checks if the current node is flagged as not needing layout
func (nb *NodeBase) HasNoLayout() bool {
return nb.HasFlag(int(NoLayout))
}
// CanFocus checks if this node can receive keyboard focus
func (nb *NodeBase) CanFocus() bool {
return nb.HasFlag(int(CanFocus))
}
// HasFocus checks if the current node is flagged as having keyboard focus
func (nb *NodeBase) HasFocus() bool {
return nb.HasFlag(int(HasFocus))
}
// SetFocusState sets current HasFocus state
func (nb *NodeBase) SetFocusState(focus bool) {
nb.SetFlagState(focus, int(HasFocus))
}
// IsDragging tests if the current node is currently flagged as receiving
// dragging events -- flag set by window
func (nb *NodeBase) IsDragging() bool {
return nb.HasFlag(int(NodeDragging))
}
// IsInstaDrag tests if the current node has InstaDrag property set
func (nb *NodeBase) IsInstaDrag() bool {
return nb.HasFlag(int(InstaDrag))
}
// IsInactive tests if this node is flagged as Inactive. if so, behave (e.g.,
// ignore events except select, context menu) and style appropriately
func (nb *NodeBase) IsInactive() bool {
return nb.HasFlag(int(Inactive))
}
// IsActive tests if this node is NOT flagged as Inactive.
func (nb *NodeBase) IsActive() bool {
return !nb.IsInactive()
}
// SetInactive sets the node as inactive
func (nb *NodeBase) SetInactive() {
nb.SetFlag(int(Inactive))
}
// ClearInactive clears the node as inactive
func (nb *NodeBase) ClearInactive() {
nb.ClearFlag(int(Inactive))
}
// SetInactiveState sets flag as inactive or not based on inact arg
func (nb *NodeBase) SetInactiveState(inact bool) {
nb.SetFlagState(inact, int(Inactive))
}
// SetActiveState sets flag as active or not based on act arg -- positive logic
// is easier to understand.
func (nb *NodeBase) SetActiveState(act bool) {
nb.SetFlagState(!act, int(Inactive))
}
// SetInactiveStateUpdt sets flag as inactive or not based on inact arg, and
// does UpdateSig if state changed.
func (nb *NodeBase) SetInactiveStateUpdt(inact bool) {
cur := nb.IsInactive()
nb.SetFlagState(inact, int(Inactive))
if inact != cur {
nb.UpdateSig()
}
}
// SetActiveStateUpdt sets flag as active or not based on act arg -- positive logic
// is easier to understand -- does UpdateSig if state changed.
func (nb *NodeBase) SetActiveStateUpdt(act bool) {
cur := nb.IsActive()
nb.SetFlagState(!act, int(Inactive))
if act != cur {
nb.UpdateSig()
}
}
// IsInvisible tests if this node is flagged as Invisible. if so, do not
// render, update, interact.
func (nb *NodeBase) IsInvisible() bool {
return nb.HasFlag(int(Invisible))
}
// SetInvisible sets the node as invisible
func (nb *NodeBase) SetInvisible() {
nb.SetFlag(int(Invisible))
}
// ClearInvisible clears the node as invisible
func (nb *NodeBase) ClearInvisible() {
nb.ClearFlag(int(Invisible))
}
// SetInvisibleState sets flag as invisible or not based on invis arg
func (nb *NodeBase) SetInvisibleState(invis bool) {
nb.SetFlagState(invis, int(Invisible))
}
// SetCanFocusIfActive sets CanFocus flag only if node is active (inactive
// nodes don't need focus typically)
func (nb *NodeBase) SetCanFocusIfActive() {
nb.SetFlagState(!nb.IsInactive(), int(CanFocus))
}
// SetCanFocus sets CanFocus flag to true
func (nb *NodeBase) SetCanFocus() {
nb.SetFlag(int(CanFocus))
}
// IsSelected tests if this node is flagged as Selected
func (nb *NodeBase) IsSelected() bool {
return nb.HasFlag(int(Selected))
}
// SetSelected sets the node as selected
func (nb *NodeBase) SetSelected() {
nb.SetFlag(int(Selected))
}
// ClearSelected sets the node as not selected
func (nb *NodeBase) ClearSelected() {
nb.ClearFlag(int(Selected))
}
// SetSelectedState set flag as selected or not based on sel arg
func (nb *NodeBase) SetSelectedState(sel bool) {
nb.SetFlagState(sel, int(Selected))
}
// NeedsFullReRender checks if node has said it needs full re-render
func (nb *NodeBase) NeedsFullReRender() bool {
return nb.HasFlag(int(FullReRender))
}
// SetFullReRender sets node as needing a full ReRender
func (nb *NodeBase) SetFullReRender() {
nb.SetFlag(int(FullReRender))
}
// ClearFullReRender clears node as needing a full ReRender
func (nb *NodeBase) ClearFullReRender() {
nb.ClearFlag(int(FullReRender))
}
// IsReRenderAnchor returns whethers the current node is a ReRenderAnchor
func (nb *NodeBase) IsReRenderAnchor() bool {
return nb.HasFlag(int(ReRenderAnchor))
}
// SetReRenderAnchor sets node as a ReRenderAnchor
func (nb *NodeBase) SetReRenderAnchor() {
nb.SetFlag(int(ReRenderAnchor))
}
// PointToRelPos translates a point in global pixel coords
// into relative position within node
func (nb *NodeBase) PointToRelPos(pt image.Point) image.Point {
nb.BBoxMu.RLock()
defer nb.BBoxMu.RUnlock()
return pt.Sub(nb.WinBBox.Min)
}
// PosInWinBBox returns true if given position is within
// this node's win bbox (under read lock)
func (nb *NodeBase) PosInWinBBox(pos image.Point) bool {
nb.BBoxMu.RLock()
defer nb.BBoxMu.RUnlock()
return pos.In(nb.WinBBox)
}
// WinBBoxInBBox returns true if our BBox is contained within
// given BBox (under read lock)
func (nb *NodeBase) WinBBoxInBBox(bbox image.Rectangle) bool {
nb.BBoxMu.RLock()
defer nb.BBoxMu.RUnlock()
return mat32.RectInNotEmpty(nb.WinBBox, bbox)
}
// AddClass adds a CSS class name -- does proper space separation
func (nb *NodeBase) AddClass(cls string) {
if nb.Class == "" {
nb.Class = cls
} else {
nb.Class += " " + cls
}
}
// StyleProps returns a property that contains another map of properties for a
// given styling selector, such as :normal :active :hover etc -- the
// convention is to prefix this selector with a : and use lower-case names, so
// we follow that.
func (nb *NodeBase) StyleProps(selector string) ki.Props {
sp, ok := nb.PropInherit(selector, ki.NoInherit, ki.TypeProps) // yeah, use type's
if !ok {
return nil
}
spm, ok := sp.(ki.Props)
if ok {
return spm
}
log.Printf("gist.StyleProps: looking for a ki.Props for style selector: %v, instead got type: %T, for node: %v\n", selector, spm, nb.Path())
return nil
}
// AggCSS aggregates css properties
func AggCSS(agg *ki.Props, css ki.Props) {
if *agg == nil {
*agg = make(ki.Props, len(css))
}
for key, val := range css {
(*agg)[key] = val
}
}
// ParentCSSAgg returns parent's CSSAgg styles or nil if not avail
func (nb *NodeBase) ParentCSSAgg() *ki.Props {
if nb.Par == nil {
return nil
}
pn := nb.Par.Embed(KiT_NodeBase).(*NodeBase)
return &pn.CSSAgg
}
// SetStdXMLAttr sets standard attributes of node given XML-style name /
// attribute values (e.g., from parsing XML / SVG files) -- returns true if handled
func SetStdXMLAttr(ni Node, name, val string) bool {
nb := ni.AsGiNode()
switch name {
case "id":
nb.SetName(val)
return true
case "class":
nb.Class = val
return true
case "style":
gist.SetStylePropsXML(val, &nb.Props)
return true
}
return false
}
// FirstContainingPoint finds the first node whose WinBBox contains the given
// point -- nil if none. If leavesOnly is set then only nodes that have no
// nodes (leaves, terminal nodes) will be considered
func (nb *NodeBase) FirstContainingPoint(pt image.Point, leavesOnly bool) ki.Ki {
var rval ki.Ki
nb.FuncDownMeFirst(0, nb.This(), func(k ki.Ki, level int, d any) bool {
if k == nb.This() {
return ki.Continue
}
if leavesOnly && k.HasChildren() {
return ki.Continue
}
_, ni := KiToNode2D(k)
if ni == nil || ni.IsDeleted() || ni.IsDestroyed() {
// 3D?
return ki.Break
}
if ni.PosInWinBBox(pt) {
rval = ni.This()
return ki.Break
}
return ki.Continue
})
return rval
}
// AllWithinBBox returns a list of all nodes whose WinBBox is fully contained
// within the given BBox. If leavesOnly is set then only nodes that have no
// nodes (leaves, terminal nodes) will be considered.
func (nb *NodeBase) AllWithinBBox(bbox image.Rectangle, leavesOnly bool) ki.Slice {
var rval ki.Slice
nb.FuncDownMeFirst(0, nb.This(), func(k ki.Ki, level int, d any) bool {
if k == nb.This() {
return ki.Continue
}
if leavesOnly && k.HasChildren() {
return ki.Continue
}
_, ni := KiToNode2D(k)
if ni == nil || ni.IsDeleted() || ni.IsDestroyed() {
// 3D?
return ki.Break
}
if ni.WinBBoxInBBox(bbox) {
rval = append(rval, ni.This())
}
return ki.Continue
})
return rval
}
// standard css properties on nodes apply, including visible, etc.
// see node2d.go for 2d node