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)
242 lines
5.5 KiB
Go
242 lines
5.5 KiB
Go
package richtext
|
|
|
|
import (
|
|
"aego/core/mathx"
|
|
"aego/render/text"
|
|
"aego/ui/core"
|
|
"aego/ui/theme"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
type fonts struct {
|
|
face [H3 + 1]*text.Font
|
|
}
|
|
|
|
func (f *fonts) Width(face Face, s string) float32 {
|
|
return f.face[face].Measure(s).X
|
|
}
|
|
|
|
func (f *fonts) Height(face Face) float32 {
|
|
return f.face[face].LineHeight
|
|
}
|
|
|
|
func pick(ui *core.Context) *fonts {
|
|
body := ui.Font()
|
|
fs := &fonts{}
|
|
fs.face[Body] = body
|
|
fs.face[Mono] = ui.MonoFont()
|
|
for i, k := range [...]float32{1.6, 1.3, 1.1} {
|
|
f := body
|
|
if set := ui.Fonts(); set != nil {
|
|
if c := set.Closest(ui.Theme.FontSize*k, ui.Scale()); c != nil {
|
|
f = c
|
|
}
|
|
}
|
|
fs.face[H1+Face(i)] = f
|
|
}
|
|
return fs
|
|
}
|
|
|
|
type Action struct {
|
|
Target string
|
|
Run int
|
|
}
|
|
|
|
type Text struct {
|
|
Highlight Highlighter
|
|
RunButtons bool
|
|
Scroll float32
|
|
blocks []Block
|
|
doc *Doc
|
|
width float32
|
|
key *text.Font
|
|
}
|
|
|
|
func (t *Text) Set(blocks []Block) {
|
|
t.blocks, t.doc, t.Scroll = blocks, nil, 0
|
|
}
|
|
|
|
func (t *Text) Replace(blocks []Block) {
|
|
t.blocks, t.doc = blocks, nil
|
|
}
|
|
|
|
func (t *Text) Blocks() []Block {
|
|
return t.blocks
|
|
}
|
|
|
|
func (t *Text) Doc() *Doc {
|
|
return t.doc
|
|
}
|
|
|
|
func tokenColor(c *theme.CodeTheme, kind string) mathx.Color {
|
|
switch kind {
|
|
case "keyword":
|
|
return c.Keyword
|
|
case "type":
|
|
return c.Type
|
|
case "string":
|
|
return c.String
|
|
case "number":
|
|
return c.Number
|
|
case "comment":
|
|
return c.Comment
|
|
case "attr":
|
|
return c.Attribute
|
|
case "builtin":
|
|
return c.Builtin
|
|
case "func":
|
|
return c.Function
|
|
}
|
|
return c.Plain
|
|
}
|
|
|
|
func runColor(th *theme.Theme, l *Line, r *Run) mathx.Color {
|
|
switch {
|
|
case l.Kind == CodeLine:
|
|
return tokenColor(&th.Code, r.Token)
|
|
case r.Target != "":
|
|
return th.Accent
|
|
case r.Style == Code:
|
|
return th.Code.Plain
|
|
case r.Style == Emphasis:
|
|
return th.TextDim
|
|
}
|
|
return th.Text
|
|
}
|
|
|
|
func (t *Text) background(ui *core.Context, l *Line, x, y, w float32) {
|
|
th := &ui.Theme
|
|
switch l.Kind {
|
|
case CodeLine:
|
|
top, h := y, l.H
|
|
if l.First {
|
|
top -= CodePad
|
|
h += CodePad
|
|
}
|
|
if l.Last {
|
|
h += CodePad
|
|
}
|
|
ui.Rect(mathx.Rect{X: x, Y: top, W: w, H: h}, th.Code.Background)
|
|
case TableLine:
|
|
cols := l.Cols
|
|
right := x + cols[len(cols)-1]
|
|
top, h := y, l.H
|
|
if l.RowTop {
|
|
top -= 3
|
|
h += 3
|
|
ui.Rect(mathx.Rect{X: x, Y: top, W: cols[len(cols)-1], H: 1}, th.Border)
|
|
}
|
|
if l.Last {
|
|
h += 3
|
|
ui.Rect(mathx.Rect{X: x, Y: top + h, W: cols[len(cols)-1], H: 1}, th.Border)
|
|
}
|
|
if l.Header {
|
|
ui.Rect(mathx.Rect{X: x, Y: top, W: cols[len(cols)-1], H: h}, th.PanelAlt)
|
|
}
|
|
for _, cx := range cols {
|
|
ui.Rect(mathx.Rect{X: min(x+cx, right-1), Y: top, W: 1, H: h + 1}, th.Border)
|
|
}
|
|
case QuoteLine:
|
|
ui.Rect(mathx.Rect{X: x + 4, Y: y, W: 2, H: l.H}, th.AccentDim)
|
|
case HeadingLine:
|
|
if l.Level <= 2 {
|
|
ui.Rect(mathx.Rect{X: x, Y: y + l.H + 1, W: w, H: 1}, th.Separator)
|
|
}
|
|
case RuleLine:
|
|
ui.Rect(mathx.Rect{X: x, Y: y + l.H/2, W: w, H: 1}, th.Border)
|
|
}
|
|
}
|
|
|
|
func (t *Text) Draw(ui *core.Context, id core.ID, r mathx.Rect) Action {
|
|
act := Action{Run: -1}
|
|
if ui.Font() == nil {
|
|
return act
|
|
}
|
|
th := &ui.Theme
|
|
fs := pick(ui)
|
|
pad := th.Padding * 2
|
|
width := max(40, r.W-2*pad-th.ScrollbarW)
|
|
if t.doc == nil || width != t.width || fs.face[Body] != t.key {
|
|
t.doc = Layout(t.blocks, width, fs, t.Highlight)
|
|
t.width, t.key = width, fs.face[Body]
|
|
}
|
|
d := t.doc
|
|
content := d.Height + 2*pad
|
|
maxOff := max(0, content-r.H)
|
|
if ui.MouseOver(id, r) {
|
|
if w := ui.WheelY(); w != 0 {
|
|
t.Scroll -= w * th.RowHeight * 3
|
|
}
|
|
}
|
|
t.Scroll = mathx.Clamp(t.Scroll, 0, maxOff)
|
|
ox, oy := r.X+pad, r.Y+pad-t.Scroll
|
|
ui.PushClip(r)
|
|
link := 0
|
|
for i := range d.Lines {
|
|
l := &d.Lines[i]
|
|
y := oy + l.Y
|
|
if y-CodePad > r.Y+r.H {
|
|
break
|
|
}
|
|
if y+l.H+CodePad < r.Y {
|
|
for j := range l.Runs {
|
|
if l.Runs[j].Target != "" {
|
|
link++
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
t.background(ui, l, ox, y, width)
|
|
for j := range l.Runs {
|
|
run := &l.Runs[j]
|
|
f := fs.face[run.Face]
|
|
ui.PushFont(f)
|
|
pos := mathx.Vec2{X: ox + run.X, Y: y + (l.H-f.LineHeight)/2}
|
|
rect := mathx.Rect{X: pos.X, Y: y, W: run.W, H: l.H}
|
|
if run.Style == Code && l.Kind != CodeLine {
|
|
ui.Rect(mathx.Rect{X: rect.X - 2, Y: rect.Y + 1, W: rect.W + 4, H: rect.H - 2}, th.Code.Background)
|
|
}
|
|
col := runColor(th, l, run)
|
|
if run.Target != "" {
|
|
lid := core.IDSeq(id, 1000+link)
|
|
link++
|
|
if ui.Clicked(lid, rect) {
|
|
act.Target = run.Target
|
|
}
|
|
if ui.Hot() == lid {
|
|
ui.Rect(mathx.Rect{X: rect.X, Y: rect.Y + rect.H - 2, W: rect.W, H: 1}, th.Accent)
|
|
}
|
|
}
|
|
ui.Text(run.Text, pos, col)
|
|
if run.Style == Strong || l.Kind == HeadingLine || l.Header {
|
|
ui.Text(run.Text, mathx.Vec2{X: pos.X + 0.6, Y: pos.Y}, col)
|
|
}
|
|
ui.PopFont()
|
|
}
|
|
if l.Run && t.RunButtons {
|
|
bw := ui.TextSize("Run").X + 16
|
|
br := mathx.Rect{X: ox + width - bw - 4, Y: y - CodePad + 4, W: bw, H: th.RowHeight + 2}
|
|
if widgets.ButtonRect(ui, core.IDSeq(id, 50000+l.Block), br, "Run") {
|
|
act.Run = l.Block
|
|
}
|
|
}
|
|
}
|
|
ui.PopClip()
|
|
if content > r.H {
|
|
track := mathx.Rect{X: r.X + r.W - th.ScrollbarW, Y: r.Y, W: th.ScrollbarW, H: r.H}
|
|
ui.Rect(track, th.PanelAlt)
|
|
thumbH := max(20, r.H*r.H/content)
|
|
thumb := mathx.Rect{X: track.X + 1, Y: r.Y + (t.Scroll/maxOff)*(r.H-thumbH), W: track.W - 2, H: thumbH}
|
|
tid := core.IDSeq(id, 1)
|
|
if dlt, ok := ui.Drag(tid, thumb); ok && dlt.Y != 0 && r.H > thumbH {
|
|
t.Scroll = mathx.Clamp(t.Scroll+dlt.Y*maxOff/(r.H-thumbH), 0, maxOff)
|
|
}
|
|
col := th.Widget
|
|
if ui.Hot() == tid || ui.Active() == tid {
|
|
col = th.WidgetHot
|
|
}
|
|
ui.Rect(thumb, col)
|
|
}
|
|
return act
|
|
}
|