1// Package ui owns the immediate-mode frame tree, controls, editors, input
2// routing and the Renderer behind the public nefergui facade.
3package ui
4
5import (
6 "fmt"
7 "image"
8
9 "github.com/bnema/nefergui/internal/css"
10 "github.com/bnema/nefergui/internal/edit"
11 "github.com/bnema/nefergui/internal/layout"
12)
13
14// Identity is a path of sibling-local keys or positional element identities.
15// It is opaque to applications; ID is CSS metadata, not identity.
16type identity struct {
17 parent *identity
18 key string
19 position int
20 typ string
21 explicit bool
22 path string // idKey result, set when the identity is built
23}
24
25func (i *identity) same(j *identity) bool {
26 if i == nil || j == nil {
27 return i == j
28 }
29 return i.key == j.key && i.position == j.position && i.typ == j.typ && i.explicit == j.explicit && i.parent.same(j.parent)
30}
31
32// Frame is valid only during one call to the view function.
33type Frame struct {
34 generation uint64
35 active bool
36 root *element
37 previous *element
38 events []inputEvent
39 styles *css.Engine
40 edits map[string]*edit.State
41 clipboard edit.Clipboard
42 owner *runtime
43 arena *elementArena
44 ime edit.IME
45 state interactionState
46 layout layout.Output
47 diagnostics []string
48 width, height float64
49}
50
51// Node is an ephemeral handle into the current frame. Do not retain it between frames.
52type Node struct {
53 frame *Frame
54 element *element
55 generation uint64
56}
57type element struct {
58 identity *identity
59 typ, role, id string
60 key string // Key option, applied to identity when the element is built
61 hasKey bool
62 classes []string
63 inline *css.Declarations
64 computed *css.Computed
65 parent *element
66 children []*element
67 prev *element // previous-frame counterpart while building; its identity is reused
68 usedKeys map[string]bool // debug builds only
69 events []inputEvent
70 disabled bool
71 checked bool
72 fraction float64 // slider value position in [0,1]; negative when unknown
73 placeholder string
74 text string
75 password bool
76 imageSize layout.Size
77 image image.Image
78 level int
79 rect layout.Rect // absolute geometry, see Node.Rect
80 hasRect bool
81 ln layout.Node // reused by layoutTree; never read after layout.Layout returns
82}
83
84func (n Node) valid() bool {
85 return n.frame != nil && n.frame.active && n.generation == n.frame.generation && n.element != nil
86}
87func (n Node) check() bool {
88 if n.valid() {
89 return true
90 }
91 if debugDiagnostics {
92 panic("nefergui: stale Node")
93 }
94 return false
95}
96func (f *Frame) Root(options ...ContainerOption) Node {
97 if !f.active {
98 panic("nefergui: Root outside frame")
99 }
100 if f.root != nil {
101 panic("nefergui: Root called twice")
102 }
103 root := f.arena.newElement()
104 root.typ = "app"
105 applyContainer(root, options)
106 if root.hasKey {
107 root.identity = &identity{key: root.key}
108 } else if p := f.previous; p != nil && p.identity != nil && p.identity.typ == "app" && p.identity.parent == nil && !p.identity.explicit {
109 root.identity, root.prev = p.identity, p
110 } else {
111 root.identity = &identity{typ: "app", path: "root"}
112 }
113 f.root = root
114 n := Node{f, root, f.generation}
115 f.compute(root)
116 f.deliver(root)
117 return n
118}
119func (f *Frame) compute(e *element) {
120 if f.styles != nil {
121 var p *css.Computed
122 if e.parent != nil {
123 p = e.parent.computed
124 }
125 e.computed = f.styles.Compute(&css.Element{Type: e.typ, ID: e.id, Classes: e.classes, Inline: e.inline, State: f.state.flags(e)}, p)
126 }
127}
128
129// refresh recomputes styles after the view ran and drops the previous-tree
130// links, which only serve identity reuse while building.
131func (f *Frame) refresh(e *element) {
132 f.compute(e)
133 e.prev = nil
134 for _, ch := range e.children {
135 f.refresh(ch)
136 }
137}
138func (f *Frame) deliver(e *element) {
139 for _, ev := range f.events {
140 if ev.Target.same(e.identity) {
141 e.events = append(e.events, ev)
142 }
143 }
144}
145func (n Node) child(typ, role string, options []ContainerOption) Node {
146 if !n.check() {
147 return Node{}
148 }
149 e := n.frame.arena.newElement()
150 e.typ, e.role, e.parent = typ, role, n.element
151 applyContainer(e, options)
152 position := len(n.element.children)
153 i := n.reuseIdentity(e, position)
154 if i == nil {
155 i = &identity{parent: n.element.identity, typ: typ, position: position}
156 if e.hasKey {
157 i.explicit = true
158 i.key = e.key
159 i.position = 0
160 i.typ = ""
161 }
162 i.path = identityPath(i)
163 } else {
164 e.prev = n.element.prev.children[position]
165 }
166 if e.hasKey && debugDiagnostics {
167 if n.element.usedKeys == nil {
168 n.element.usedKeys = make(map[string]bool)
169 }
170 if n.element.usedKeys[i.key] {
171 n.frame.diagnostics = append(n.frame.diagnostics, fmt.Sprintf("duplicate sibling key %q", i.key))
172 }
173 n.element.usedKeys[i.key] = true
174 }
175 e.identity = i
176 if debugDiagnostics && !i.explicit && n.frame.previous != nil {
177 if prior := findIdentity(n.frame.previous, n.element.identity); prior != nil && i.position < len(prior.children) {
178 old := prior.children[i.position]
179 if !old.identity.explicit && old.typ != typ {
180 n.frame.diagnostics = append(n.frame.diagnostics, fmt.Sprintf("unkeyed child changed type at position %d: %s -> %s", i.position, old.typ, typ))
181 }
182 }
183 }
184 n.element.children = append(n.element.children, e)
185 n.frame.compute(e)
186 n.frame.deliver(e)
187 return Node{n.frame, e, n.generation}
188}
189
190// reuseIdentity returns the previous frame's identity for the child about to
191// be appended at position when it denotes the same path, so a steady frame
192// neither allocates identities nor rebuilds their path strings. It only looks
193// at the same sibling position; any other case builds a fresh identity, which
194// compares equal by value, so behaviour is unchanged. n.element.prev is set
195// only when n.element reuses its own previous identity, which makes pointer
196// equality of the parent a complete parent comparison.
197func (n Node) reuseIdentity(e *element, position int) *identity {
198 p := n.element.prev
199 if p == nil || position >= len(p.children) {
200 return nil
201 }
202 old := p.children[position].identity
203 if old == nil || old.parent != n.element.identity {
204 return nil
205 }
206 if e.hasKey {
207 if !old.explicit || old.key != e.key {
208 return nil
209 }
210 } else if old.explicit || old.position != position || old.typ != e.typ {
211 return nil
212 }
213 return old
214}
215func findIdentity(e *element, id *identity) *element {
216 if e == nil {
217 return nil
218 }
219 if e.identity.same(id) {
220 return e
221 }
222 for _, child := range e.children {
223 if found := findIdentity(child, id); found != nil {
224 return found
225 }
226 }
227 return nil
228}
229func (n Node) Element(typ string, options ...ContainerOption) Node { return n.child(typ, "", options) }
230func (n Node) Box(options ...ContainerOption) Node { return n.child("box", "", options) }
231func (n Node) Row(options ...ContainerOption) Node { return n.child("row", "", options) }
232func (n Node) Column(options ...ContainerOption) Node { return n.child("column", "", options) }
233func (n Node) Stack(options ...ContainerOption) Node { return n.child("stack", "", options) }
234func (n Node) Scroll(options ...ContainerOption) Node { return n.child("scroll", "", options) }
235func (n Node) Header(options ...ContainerOption) Node { return n.child("header", "banner", options) }
236func (n Node) Nav(options ...ContainerOption) Node { return n.child("nav", "navigation", options) }
237func (n Node) Main(options ...ContainerOption) Node { return n.child("main", "main", options) }
238func (n Node) Section(options ...ContainerOption) Node { return n.child("section", "region", options) }
239func (n Node) Aside(options ...ContainerOption) Node {
240 return n.child("aside", "complementary", options)
241}
242func (n Node) Footer(options ...ContainerOption) Node {
243 return n.child("footer", "contentinfo", options)
244}
245
246// Diagnostics returns a copy of frame diagnostics (populated with -tags nefergui_debug).
247func (f *Frame) Diagnostics() []string { return append([]string(nil), f.diagnostics...) }
248
249// Size is the logical surface size this frame is laid out against.
250func (f *Frame) Size() (width, height float64) { return f.width, f.height }
251
252// Rect positions the node at x, y with size w, h in logical pixels, relative
253// to the content box of its Stack parent, without CSS parsing or per-frame
254// strings or style copies. The geometry is border-box and overrides authored
255// width, height and margin. Rect is valid only on a direct child of Stack;
256// elsewhere it is ignored, keeping normal flex and block layout, and debug
257// builds (-tags nefergui_debug) report it through Frame.Diagnostics.
258func (n Node) Rect(x, y, w, h float64) Node {
259 if !n.check() {
260 return n
261 }
262 if p := n.element.parent; p == nil || p.typ != "stack" {
263 if debugDiagnostics {
264 n.frame.diagnostics = append(n.frame.diagnostics, "Rect used on a node whose parent is not a Stack")
265 }
266 return n
267 }
268 n.element.rect, n.element.hasRect = layout.Rect{X: x, Y: y, W: w, H: h}, true
269 return n
270}