Files
sanspie 69d19e501f Stage 7: documentation, help, playground; templates, aego new/bootstrap/icons, launcher
Documentation: engine/docs assembles one registry from the script
bindings, the schema, module docs and module-owned script paths; problems
are reported as warnings at build time. Module registries record doc
sources and script module owners; diag.Codes lists every diagnostic code.

Project bootstrap: project.defaultSpace ("2d"/"3d"), EnsureDirs creates
the default folders when the editor opens a project (reported in the
console), prefabs/ is a pipeline root. Native builds drop -trimpath so
binding locations resolve inside native/.

Editor files: create scene/prefab/script from menus, move/rename/duplicate
/delete with undo, script package retargeting on move, open scenes and
script panels follow renames, Save As / Rename Scene prompts, scene
picker, startup scene. Scoped shortcuts fall back to the global command
when disabled; Save As is Ctrl+Shift+S, Save All Ctrl+Alt+S.

Script editor: Go to Implementation (stub → native/ source), host
definitions always resolve to stubs, bracket match/mismatch states,
rainbow brackets (persisted), mono code font (ui-mono atlases, per-command
fonts in draw.List, PushFont/PopFont restored by Mark/Restore).

Tests: langsvc definition/implementation, bracket states, dispatch
fallback, script renames, font stack, editor file-op integration, golden
mono glyph and bracket references; depcheck rule for engine/docs.

Docs pipeline: engine, render2d and script/engine ship their own res/docs;
keyword hover text comes from the registry through manifest.Keywords;
project script symbols join the registry; aego docs export|check|build|
serve, --tool docs in native binaries, aego check --explain; app/docstool
and the tools/gendocs module (goldmark) build the static site.

Playground: app/playground + cmd/aego-playground (js/wasm, 3.3 MB gzip)
compile, highlight and run scripts in the browser; run blocks in the
language docs become examples. engine/builtin/headless is the module set
without render3d; workspace.Config.FS; StubFor/StubPaths; backends and the
script importer drop out of js builds.

VM: one frame pointer in the interpreter loop, direct host calls, panic
recovery once per run instead of per host call (Update2k -19%,
Schedule10k -24% against the previous head). bench-build/compare/profile
run pre-built, pinned, interleaved binaries.

Editor: structural bracket levels and current-block highlight, indent
guides, occurrence highlighting with write marks, multi-format clipboard
(copy/cut/paste assets, paste images as sprites or textures). gl33 probes
a renderable depth format and reports framebuffer failures by stage.

7 E:
- project templates empty/2d/2.5d/3d, app/projects as the single creation path (D148)
- launcher and recent projects (D149)
- orthographic Camera3D and Scene View (D150)
- reference states in the Inspector, ids.Short (D151)
- sceneio remaps node references when loading without KeepIDs (D152)
- exported ComponentRef<T> (D153)
- aego icons via module.Audit (D146)
2026-10-13 01:08:56 +03:00

338 lines
6.6 KiB
Go

package richtext
import (
"sort"
"strconv"
"strings"
)
type Face uint8
const (
Body Face = iota
Mono
H1
H2
H3
)
type Measurer interface {
Width(f Face, s string) float32
Height(f Face) float32
}
type Token struct {
Start int
End int
Kind string
}
type Highlighter func(lang, line string) []Token
type Run struct {
Text string
X float32
W float32
Face Face
Style Style
Target string
Token string
}
type LineKind uint8
const (
TextLine LineKind = iota
HeadingLine
CodeLine
TableLine
RuleLine
QuoteLine
)
type Line struct {
Kind LineKind
Y float32
H float32
Runs []Run
Block int
First bool
Last bool
Header bool
RowTop bool
Cols []float32
Level int
Run bool
}
type Doc struct {
Lines []Line
Height float32
Width float32
}
const (
BlockGap = 8
ListIndent = 18
CodePad = 8
CellPad = 6
QuoteInset = 14
MinColumn = 48
)
type piece struct {
text string
style Style
target string
face Face
space bool
}
func pieces(spans []Span, face Face) []piece {
var out []piece
for _, sp := range spans {
f := face
if sp.Style == Code {
f = Mono
}
s := sp.Text
for s != "" {
if s[0] == ' ' {
j := 0
for j < len(s) && s[j] == ' ' {
j++
}
out = append(out, piece{text: " ", style: sp.Style, target: sp.Target, face: f, space: true})
s = s[j:]
continue
}
j := strings.IndexByte(s, ' ')
if j < 0 {
j = len(s)
}
out = append(out, piece{text: s[:j], style: sp.Style, target: sp.Target, face: f})
s = s[j:]
}
}
return out
}
func wrap(spans []Span, face Face, x0, width float32, m Measurer) [][]Run {
var lines [][]Run
var cur []Run
x := x0
limit := x0 + width
for _, p := range pieces(spans, face) {
if p.space && len(cur) == 0 {
continue
}
w := m.Width(p.face, p.text)
if !p.space && len(cur) > 0 && x+w > limit {
if last := &cur[len(cur)-1]; strings.HasSuffix(last.Text, " ") {
last.Text = strings.TrimRight(last.Text, " ")
last.W = m.Width(last.Face, last.Text)
}
lines = append(lines, cur)
cur, x = nil, x0
}
if n := len(cur); n > 0 {
last := &cur[n-1]
if last.Style == p.style && last.Target == p.target && last.Face == p.face {
last.Text += p.text
last.W = m.Width(last.Face, last.Text)
x = last.X + last.W
continue
}
}
cur = append(cur, Run{Text: p.text, X: x, W: w, Face: p.face, Style: p.style, Target: p.target})
x += w
}
if len(cur) > 0 {
lines = append(lines, cur)
}
return lines
}
func headingFace(level int) Face {
switch level {
case 1:
return H1
case 2:
return H2
}
return H3
}
type builder struct {
m Measurer
hl Highlighter
width float32
doc Doc
y float32
}
func (b *builder) add(l Line) {
l.Y = b.y
b.doc.Lines = append(b.doc.Lines, l)
b.y += l.H
}
func (b *builder) text(block int, kind LineKind, spans []Span, face Face, x float32, level int) {
h := b.m.Height(face)
for _, runs := range wrap(spans, face, x, b.width-x, b.m) {
b.add(Line{Kind: kind, H: h, Runs: runs, Block: block, Level: level})
}
}
func (b *builder) code(block int, blk *Block) {
h := b.m.Height(Mono)
b.y += CodePad
start := len(b.doc.Lines)
lines := blk.Lines
if len(lines) == 0 {
lines = []string{""}
}
for _, src := range lines {
src = strings.ReplaceAll(src, "\t", " ")
l := Line{Kind: CodeLine, H: h, Block: block}
x := float32(CodePad)
var toks []Token
if b.hl != nil {
toks = b.hl(blk.Lang, src)
sort.Slice(toks, func(i, j int) bool { return toks[i].Start < toks[j].Start })
}
at := 0
emit := func(s, kind string) {
if s == "" {
return
}
w := b.m.Width(Mono, s)
l.Runs = append(l.Runs, Run{Text: s, X: x, W: w, Face: Mono, Style: Code, Token: kind})
x += w
}
for _, t := range toks {
if t.Start < at || t.End > len(src) || t.End <= t.Start {
continue
}
emit(src[at:t.Start], "")
emit(src[t.Start:t.End], t.Kind)
at = t.End
}
emit(src[at:], "")
b.add(l)
}
b.doc.Lines[start].First = true
b.doc.Lines[start].Run = blk.Runnable()
b.doc.Lines[len(b.doc.Lines)-1].Last = true
b.y += CodePad
}
func (b *builder) table(block int, blk *Block) {
cols := 0
for _, r := range blk.Rows {
cols = max(cols, len(r))
}
if cols == 0 {
return
}
natural := make([]float32, cols)
for _, r := range blk.Rows {
for c, cell := range r {
w := float32(0)
for _, p := range pieces(cell, Body) {
w += b.m.Width(p.face, p.text)
}
natural[c] = max(natural[c], w+2*CellPad)
}
}
total := float32(0)
for _, w := range natural {
total += w
}
widths := natural
if total > b.width {
widths = make([]float32, cols)
for c, w := range natural {
widths[c] = max(MinColumn, w*b.width/total)
}
}
xs := make([]float32, cols+1)
for c := range widths {
xs[c+1] = xs[c] + widths[c]
}
h := b.m.Height(Body)
for ri, r := range blk.Rows {
var cellLines [][][]Run
rows := 1
for c := 0; c < cols; c++ {
var cl [][]Run
if c < len(r) {
cl = wrap(r[c], Body, xs[c]+CellPad, widths[c]-2*CellPad, b.m)
}
cellLines = append(cellLines, cl)
rows = max(rows, len(cl))
}
b.y += 3
for li := 0; li < rows; li++ {
l := Line{Kind: TableLine, H: h, Block: block, Header: blk.Header && ri == 0, Cols: xs}
if li == 0 {
l.RowTop = true
}
for _, cl := range cellLines {
if li < len(cl) {
l.Runs = append(l.Runs, cl[li]...)
}
}
b.add(l)
}
b.doc.Lines[len(b.doc.Lines)-1].Last = true
b.y += 3
}
}
func Layout(blocks []Block, width float32, m Measurer, hl Highlighter) *Doc {
b := &builder{m: m, hl: hl, width: max(width, 40)}
for i := range blocks {
blk := &blocks[i]
if i > 0 {
b.y += BlockGap
if blk.Kind == Heading {
b.y += BlockGap
}
}
switch blk.Kind {
case Heading:
b.text(i, HeadingLine, blk.Spans, headingFace(blk.Level), 0, blk.Level)
case Paragraph:
b.text(i, TextLine, blk.Spans, Body, 0, 0)
case Quote:
b.text(i, QuoteLine, blk.Spans, Body, QuoteInset, 0)
case ListItem:
if i > 0 && blocks[i-1].Kind == ListItem {
b.y -= BlockGap * 3 / 4
}
x := float32(blk.Level+1) * ListIndent
mark := "•"
if blk.Ordinal > 0 {
mark = strconv.Itoa(blk.Ordinal) + "."
}
start := len(b.doc.Lines)
b.text(i, TextLine, blk.Spans, Body, x, blk.Level)
if start < len(b.doc.Lines) {
l := &b.doc.Lines[start]
mw := m.Width(Body, mark)
l.Runs = append([]Run{{Text: mark, X: x - 6 - mw, W: mw, Face: Body}}, l.Runs...)
}
case CodeBlock:
b.code(i, blk)
case Table:
b.table(i, blk)
case Rule:
b.add(Line{Kind: RuleLine, H: BlockGap * 2, Block: i})
}
}
b.doc.Height = b.y
b.doc.Width = b.width
return &b.doc
}