v0.1.0 47aaa0f

tree.go RawHistoryFolder

1// Package ui owns the immediate-mode frame tree, controls, editors, input
2// routing and the Wayland run loop 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}
48
49// Node is an ephemeral handle into the current frame. Do not retain it between frames.
50type Node struct {
51	frame      *Frame
52	element    *element
53	generation uint64
54}
55type element struct {
56	identity      *identity
57	typ, role, id string
58	classes       []string
59	inline        *css.Declarations
60	computed      *css.Computed
61	parent        *element
62	children      []*element
63	usedKeys      map[string]bool
64	events        []inputEvent
65	disabled      bool
66	checked       bool
67	fraction      float64 // slider value position in [0,1]; negative when unknown
68	placeholder   string
69	text          string
70	password      bool
71	imageSize     layout.Size
72	image         image.Image
73	level         int
74}
75
76func (n Node) valid() bool {
77	return n.frame != nil && n.frame.active && n.generation == n.frame.generation && n.element != nil
78}
79func (n Node) check() bool {
80	if n.valid() {
81		return true
82	}
83	if debugDiagnostics {
84		panic("nefergui: stale Node")
85	}
86	return false
87}
88func (f *Frame) Root(options ...ContainerOption) Node {
89	if !f.active {
90		panic("nefergui: Root outside frame")
91	}
92	if f.root != nil {
93		panic("nefergui: Root called twice")
94	}
95	root := &element{typ: "app", identity: &identity{typ: "app", path: "root"}, usedKeys: make(map[string]bool)}
96	f.root = root
97	n := Node{f, root, f.generation}
98	applyContainer(root, options)
99	f.compute(root)
100	f.deliver(root)
101	return n
102}
103func (f *Frame) compute(e *element) {
104	if f.styles != nil {
105		var p *css.Computed
106		if e.parent != nil {
107			p = e.parent.computed
108		}
109		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)
110	}
111}
112func (f *Frame) deliver(e *element) {
113	for _, ev := range f.events {
114		if ev.Target.same(e.identity) {
115			e.events = append(e.events, ev)
116		}
117	}
118}
119func (n Node) child(typ, role string, options []ContainerOption) Node {
120	if !n.check() {
121		return Node{}
122	}
123	e := &element{typ: typ, role: role, parent: n.element, usedKeys: make(map[string]bool)}
124	applyContainer(e, options)
125	i := &identity{parent: n.element.identity, typ: typ, position: len(n.element.children)}
126	if e.identity != nil {
127		i.explicit = true
128		i.key = e.identity.key
129		i.position = 0
130		i.typ = ""
131		if n.element.usedKeys[i.key] && debugDiagnostics {
132			n.frame.diagnostics = append(n.frame.diagnostics, fmt.Sprintf("duplicate sibling key %q", i.key))
133		}
134		n.element.usedKeys[i.key] = true
135	}
136	i.path = identityPath(i)
137	e.identity = i
138	if debugDiagnostics && !i.explicit && n.frame.previous != nil {
139		if prior := findIdentity(n.frame.previous, n.element.identity); prior != nil && i.position < len(prior.children) {
140			old := prior.children[i.position]
141			if !old.identity.explicit && old.typ != typ {
142				n.frame.diagnostics = append(n.frame.diagnostics, fmt.Sprintf("unkeyed child changed type at position %d: %s -> %s", i.position, old.typ, typ))
143			}
144		}
145	}
146	n.element.children = append(n.element.children, e)
147	n.frame.compute(e)
148	n.frame.deliver(e)
149	return Node{n.frame, e, n.generation}
150}
151func findIdentity(e *element, id *identity) *element {
152	if e == nil {
153		return nil
154	}
155	if e.identity.same(id) {
156		return e
157	}
158	for _, child := range e.children {
159		if found := findIdentity(child, id); found != nil {
160			return found
161		}
162	}
163	return nil
164}
165func (n Node) Element(typ string, options ...ContainerOption) Node { return n.child(typ, "", options) }
166func (n Node) Box(options ...ContainerOption) Node                 { return n.child("box", "", options) }
167func (n Node) Row(options ...ContainerOption) Node                 { return n.child("row", "", options) }
168func (n Node) Column(options ...ContainerOption) Node              { return n.child("column", "", options) }
169func (n Node) Stack(options ...ContainerOption) Node               { return n.child("stack", "", options) }
170func (n Node) Scroll(options ...ContainerOption) Node              { return n.child("scroll", "", options) }
171func (n Node) Header(options ...ContainerOption) Node              { return n.child("header", "banner", options) }
172func (n Node) Nav(options ...ContainerOption) Node                 { return n.child("nav", "navigation", options) }
173func (n Node) Main(options ...ContainerOption) Node                { return n.child("main", "main", options) }
174func (n Node) Section(options ...ContainerOption) Node             { return n.child("section", "region", options) }
175func (n Node) Aside(options ...ContainerOption) Node {
176	return n.child("aside", "complementary", options)
177}
178func (n Node) Footer(options ...ContainerOption) Node {
179	return n.child("footer", "contentinfo", options)
180}
181
182// Diagnostics returns a copy of frame diagnostics (populated with -tags nefergui_debug).
183func (f *Frame) Diagnostics() []string { return append([]string(nil), f.diagnostics...) }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 12

↑↓ moveenter openesc close