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 ime edit.IME
44 state interactionState
45 layout layout.Output
46 diagnostics []string
47 width, height float64
48}
49
50// Node is an ephemeral handle into the current frame. Do not retain it between frames.
51type Node struct {
52 frame *Frame
53 element *element
54 generation uint64
55}
56type element struct {
57 identity *identity
58 typ, role, id string
59 classes []string
60 inline *css.Declarations
61 computed *css.Computed
62 parent *element
63 children []*element
64 usedKeys map[string]bool
65 events []inputEvent
66 disabled bool
67 checked bool
68 fraction float64 // slider value position in [0,1]; negative when unknown
69 placeholder string
70 text string
71 password bool
72 imageSize layout.Size
73 image image.Image
74 level int
75 rect layout.Rect // absolute geometry, see Node.Rect
76 hasRect bool
77}
78
79func (n Node) valid() bool {
80 return n.frame != nil && n.frame.active && n.generation == n.frame.generation && n.element != nil
81}
82func (n Node) check() bool {
83 if n.valid() {
84 return true
85 }
86 if debugDiagnostics {
87 panic("nefergui: stale Node")
88 }
89 return false
90}
91func (f *Frame) Root(options ...ContainerOption) Node {
92 if !f.active {
93 panic("nefergui: Root outside frame")
94 }
95 if f.root != nil {
96 panic("nefergui: Root called twice")
97 }
98 root := &element{typ: "app", identity: &identity{typ: "app", path: "root"}, usedKeys: make(map[string]bool)}
99 f.root = root
100 n := Node{f, root, f.generation}
101 applyContainer(root, options)
102 f.compute(root)
103 f.deliver(root)
104 return n
105}
106func (f *Frame) compute(e *element) {
107 if f.styles != nil {
108 var p *css.Computed
109 if e.parent != nil {
110 p = e.parent.computed
111 }
112 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)
113 }
114}
115func (f *Frame) deliver(e *element) {
116 for _, ev := range f.events {
117 if ev.Target.same(e.identity) {
118 e.events = append(e.events, ev)
119 }
120 }
121}
122func (n Node) child(typ, role string, options []ContainerOption) Node {
123 if !n.check() {
124 return Node{}
125 }
126 e := &element{typ: typ, role: role, parent: n.element, usedKeys: make(map[string]bool)}
127 applyContainer(e, options)
128 i := &identity{parent: n.element.identity, typ: typ, position: len(n.element.children)}
129 if e.identity != nil {
130 i.explicit = true
131 i.key = e.identity.key
132 i.position = 0
133 i.typ = ""
134 if n.element.usedKeys[i.key] && debugDiagnostics {
135 n.frame.diagnostics = append(n.frame.diagnostics, fmt.Sprintf("duplicate sibling key %q", i.key))
136 }
137 n.element.usedKeys[i.key] = true
138 }
139 i.path = identityPath(i)
140 e.identity = i
141 if debugDiagnostics && !i.explicit && n.frame.previous != nil {
142 if prior := findIdentity(n.frame.previous, n.element.identity); prior != nil && i.position < len(prior.children) {
143 old := prior.children[i.position]
144 if !old.identity.explicit && old.typ != typ {
145 n.frame.diagnostics = append(n.frame.diagnostics, fmt.Sprintf("unkeyed child changed type at position %d: %s -> %s", i.position, old.typ, typ))
146 }
147 }
148 }
149 n.element.children = append(n.element.children, e)
150 n.frame.compute(e)
151 n.frame.deliver(e)
152 return Node{n.frame, e, n.generation}
153}
154func findIdentity(e *element, id *identity) *element {
155 if e == nil {
156 return nil
157 }
158 if e.identity.same(id) {
159 return e
160 }
161 for _, child := range e.children {
162 if found := findIdentity(child, id); found != nil {
163 return found
164 }
165 }
166 return nil
167}
168func (n Node) Element(typ string, options ...ContainerOption) Node { return n.child(typ, "", options) }
169func (n Node) Box(options ...ContainerOption) Node { return n.child("box", "", options) }
170func (n Node) Row(options ...ContainerOption) Node { return n.child("row", "", options) }
171func (n Node) Column(options ...ContainerOption) Node { return n.child("column", "", options) }
172func (n Node) Stack(options ...ContainerOption) Node { return n.child("stack", "", options) }
173func (n Node) Scroll(options ...ContainerOption) Node { return n.child("scroll", "", options) }
174func (n Node) Header(options ...ContainerOption) Node { return n.child("header", "banner", options) }
175func (n Node) Nav(options ...ContainerOption) Node { return n.child("nav", "navigation", options) }
176func (n Node) Main(options ...ContainerOption) Node { return n.child("main", "main", options) }
177func (n Node) Section(options ...ContainerOption) Node { return n.child("section", "region", options) }
178func (n Node) Aside(options ...ContainerOption) Node {
179 return n.child("aside", "complementary", options)
180}
181func (n Node) Footer(options ...ContainerOption) Node {
182 return n.child("footer", "contentinfo", options)
183}
184
185// Diagnostics returns a copy of frame diagnostics (populated with -tags nefergui_debug).
186func (f *Frame) Diagnostics() []string { return append([]string(nil), f.diagnostics...) }
187
188// Size is the logical surface size this frame is laid out against.
189func (f *Frame) Size() (width, height float64) { return f.width, f.height }
190
191// Rect positions the node at x, y with size w, h in logical pixels, relative
192// to the content box of its Stack parent, without CSS parsing or per-frame
193// strings or style copies. The geometry is border-box and overrides authored
194// width, height and margin. Rect is valid only on a direct child of Stack;
195// elsewhere it is ignored, keeping normal flex and block layout, and debug
196// builds (-tags nefergui_debug) report it through Frame.Diagnostics.
197func (n Node) Rect(x, y, w, h float64) Node {
198 if !n.check() {
199 return n
200 }
201 if p := n.element.parent; p == nil || p.typ != "stack" {
202 if debugDiagnostics {
203 n.frame.diagnostics = append(n.frame.diagnostics, "Rect used on a node whose parent is not a Stack")
204 }
205 return n
206 }
207 n.element.rect, n.element.hasRect = layout.Rect{X: x, Y: y, W: w, H: h}, true
208 return n
209}