Files
aego/ui/richtext/draw.go
T

242 lines
5.4 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.ScrollY(th.RowHeight * 3); w != 0 {
t.Scroll -= w
}
}
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
}