Files
aego/ui/richtext/draw.go
T
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

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
}