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)
338 lines
6.6 KiB
Go
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
|
|
}
|