You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
828 lines
20 KiB
Go
828 lines
20 KiB
Go
// +build darwin,!nucular_shiny nucular_gio
|
|
|
|
package nucular
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"gioui.org/app"
|
|
"gioui.org/f32"
|
|
"gioui.org/font/opentype"
|
|
"gioui.org/io/key"
|
|
"gioui.org/io/pointer"
|
|
"gioui.org/io/profile"
|
|
"gioui.org/io/system"
|
|
"gioui.org/op"
|
|
gioclip "gioui.org/op/clip"
|
|
"gioui.org/op/paint"
|
|
"gioui.org/text"
|
|
"gioui.org/unit"
|
|
|
|
"github.com/aarzilli/nucular/clipboard"
|
|
"github.com/aarzilli/nucular/command"
|
|
"github.com/aarzilli/nucular/font"
|
|
"github.com/aarzilli/nucular/label"
|
|
"github.com/aarzilli/nucular/rect"
|
|
|
|
ifont "golang.org/x/image/font"
|
|
"golang.org/x/image/math/fixed"
|
|
mkey "golang.org/x/mobile/event/key"
|
|
"golang.org/x/mobile/event/mouse"
|
|
)
|
|
|
|
type masterWindow struct {
|
|
masterWindowCommon
|
|
|
|
Title string
|
|
initialSize image.Point
|
|
size image.Point
|
|
|
|
w *app.Window
|
|
ops op.Ops
|
|
|
|
textbuffer bytes.Buffer
|
|
|
|
closed bool
|
|
}
|
|
|
|
var clipboardStarted bool = false
|
|
var clipboardMu sync.Mutex
|
|
|
|
func NewMasterWindowSize(flags WindowFlags, title string, sz image.Point, updatefn UpdateFn) MasterWindow {
|
|
ctx := &context{}
|
|
wnd := &masterWindow{}
|
|
|
|
wnd.masterWindowCommonInit(ctx, flags, updatefn, wnd)
|
|
|
|
wnd.Title = title
|
|
wnd.initialSize = sz
|
|
|
|
clipboardMu.Lock()
|
|
if !clipboardStarted {
|
|
clipboardStarted = true
|
|
clipboard.Start()
|
|
}
|
|
clipboardMu.Unlock()
|
|
|
|
return wnd
|
|
}
|
|
|
|
func (mw *masterWindow) Main() {
|
|
go func() {
|
|
mw.w = app.NewWindow(app.Title(mw.Title), app.Size(unit.Px(float32(mw.ctx.scale(mw.initialSize.X))), unit.Px(float32(mw.ctx.scale(mw.initialSize.Y)))))
|
|
mw.main()
|
|
}()
|
|
go mw.updater()
|
|
app.Main()
|
|
}
|
|
|
|
func (mw *masterWindow) Lock() {
|
|
mw.uilock.Lock()
|
|
}
|
|
|
|
func (mw *masterWindow) Unlock() {
|
|
mw.uilock.Unlock()
|
|
}
|
|
|
|
func (mw *masterWindow) Close() {
|
|
os.Exit(0) // Bad...
|
|
}
|
|
|
|
func (mw *masterWindow) Closed() bool {
|
|
mw.uilock.Lock()
|
|
defer mw.uilock.Unlock()
|
|
return mw.closed
|
|
}
|
|
|
|
func (mw *masterWindow) main() {
|
|
for {
|
|
e := <-mw.w.Events()
|
|
switch e := e.(type) {
|
|
case system.DestroyEvent:
|
|
mw.uilock.Lock()
|
|
mw.closed = true
|
|
mw.uilock.Unlock()
|
|
if e.Err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", e.Err)
|
|
}
|
|
return
|
|
|
|
case system.FrameEvent:
|
|
mw.size = e.Size
|
|
mw.uilock.Lock()
|
|
mw.prevCmds = mw.prevCmds[:0]
|
|
mw.updateLocked()
|
|
mw.uilock.Unlock()
|
|
|
|
e.Frame(&mw.ops)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (mw *masterWindow) processPointerEvent(e pointer.Event) {
|
|
switch e.Type {
|
|
case pointer.Release, pointer.Cancel:
|
|
for i := range mw.ctx.Input.Mouse.Buttons {
|
|
btn := &mw.ctx.Input.Mouse.Buttons[i]
|
|
if btn.Down {
|
|
btn.Down = false
|
|
btn.Clicked = true
|
|
}
|
|
}
|
|
|
|
case pointer.Press:
|
|
var button mouse.Button
|
|
|
|
switch {
|
|
case e.Buttons.Contain(pointer.ButtonLeft):
|
|
button = mouse.ButtonLeft
|
|
case e.Buttons.Contain(pointer.ButtonRight):
|
|
button = mouse.ButtonRight
|
|
case e.Buttons.Contain(pointer.ButtonMiddle):
|
|
button = mouse.ButtonMiddle
|
|
}
|
|
|
|
if button == mouse.ButtonRight && e.Modifiers.Contain(key.ModCtrl) {
|
|
button = mouse.ButtonLeft
|
|
}
|
|
|
|
down := e.Type == pointer.Press
|
|
btn := &mw.ctx.Input.Mouse.Buttons[button]
|
|
if btn.Down == down {
|
|
break
|
|
}
|
|
|
|
if down {
|
|
btn.ClickedPos.X = int(e.Position.X)
|
|
btn.ClickedPos.Y = int(e.Position.Y)
|
|
}
|
|
btn.Clicked = true
|
|
btn.Down = down
|
|
|
|
case pointer.Move:
|
|
mw.ctx.Input.Mouse.Pos.X = int(e.Position.X)
|
|
mw.ctx.Input.Mouse.Pos.Y = int(e.Position.Y)
|
|
mw.ctx.Input.Mouse.Delta = mw.ctx.Input.Mouse.Pos.Sub(mw.ctx.Input.Mouse.Prev)
|
|
|
|
if e.Scroll.Y < 0 {
|
|
mw.ctx.Input.Mouse.ScrollDelta++
|
|
} else if e.Scroll.Y > 0 {
|
|
mw.ctx.Input.Mouse.ScrollDelta--
|
|
}
|
|
}
|
|
}
|
|
|
|
var runeToCode = map[string]mkey.Code{}
|
|
|
|
func init() {
|
|
for i := byte('a'); i <= 'z'; i++ {
|
|
c := mkey.Code((i - 'a') + 4)
|
|
runeToCode[string([]byte{i})] = c
|
|
runeToCode[string([]byte{i - 0x20})] = c
|
|
}
|
|
|
|
runeToCode["\t"] = mkey.CodeTab
|
|
runeToCode[" "] = mkey.CodeSpacebar
|
|
runeToCode["-"] = mkey.CodeHyphenMinus
|
|
runeToCode["="] = mkey.CodeEqualSign
|
|
runeToCode["["] = mkey.CodeLeftSquareBracket
|
|
runeToCode["]"] = mkey.CodeRightSquareBracket
|
|
runeToCode["\\"] = mkey.CodeBackslash
|
|
runeToCode[";"] = mkey.CodeSemicolon
|
|
runeToCode["\""] = mkey.CodeApostrophe
|
|
runeToCode["`"] = mkey.CodeGraveAccent
|
|
runeToCode[","] = mkey.CodeComma
|
|
runeToCode["."] = mkey.CodeFullStop
|
|
runeToCode["/"] = mkey.CodeSlash
|
|
|
|
runeToCode[key.NameLeftArrow] = mkey.CodeLeftArrow
|
|
runeToCode[key.NameRightArrow] = mkey.CodeRightArrow
|
|
runeToCode[key.NameUpArrow] = mkey.CodeUpArrow
|
|
runeToCode[key.NameDownArrow] = mkey.CodeDownArrow
|
|
runeToCode[key.NameReturn] = mkey.CodeReturnEnter
|
|
runeToCode[key.NameEnter] = mkey.CodeReturnEnter
|
|
runeToCode[key.NameEscape] = mkey.CodeEscape
|
|
runeToCode[key.NameHome] = mkey.CodeHome
|
|
runeToCode[key.NameEnd] = mkey.CodeEnd
|
|
runeToCode[key.NameDeleteBackward] = mkey.CodeDeleteBackspace
|
|
runeToCode[key.NameDeleteForward] = mkey.CodeDeleteForward
|
|
runeToCode[key.NamePageUp] = mkey.CodePageUp
|
|
runeToCode[key.NamePageDown] = mkey.CodePageDown
|
|
runeToCode[key.NameTab] = mkey.CodeTab
|
|
}
|
|
|
|
func gio2mobileKey(e key.Event) mkey.Event {
|
|
var mod mkey.Modifiers
|
|
|
|
if e.Modifiers.Contain(key.ModCommand) {
|
|
mod |= mkey.ModMeta
|
|
}
|
|
if e.Modifiers.Contain(key.ModCtrl) {
|
|
mod |= mkey.ModControl
|
|
}
|
|
if e.Modifiers.Contain(key.ModAlt) {
|
|
mod |= mkey.ModAlt
|
|
}
|
|
if e.Modifiers.Contain(key.ModSuper) {
|
|
mod |= mkey.ModMeta
|
|
}
|
|
|
|
var name rune
|
|
|
|
for _, ch := range e.Name {
|
|
name = ch
|
|
break
|
|
}
|
|
|
|
return mkey.Event{
|
|
Rune: name,
|
|
Code: runeToCode[e.Name],
|
|
Modifiers: mod,
|
|
Direction: mkey.DirRelease,
|
|
}
|
|
}
|
|
|
|
func (w *masterWindow) updater() {
|
|
var down bool
|
|
for {
|
|
if down {
|
|
time.Sleep(10 * time.Millisecond)
|
|
} else {
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
func() {
|
|
w.uilock.Lock()
|
|
defer w.uilock.Unlock()
|
|
if w.closed {
|
|
return
|
|
}
|
|
changed := atomic.LoadInt32(&w.ctx.changed)
|
|
if changed > 0 {
|
|
atomic.AddInt32(&w.ctx.changed, -1)
|
|
w.w.Invalidate()
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (mw *masterWindow) updateLocked() {
|
|
perfString := ""
|
|
q := mw.w.Queue()
|
|
for _, e := range q.Events(mw.ctx) {
|
|
switch e := e.(type) {
|
|
case profile.Event:
|
|
perfString = e.Timings
|
|
case pointer.Event:
|
|
mw.processPointerEvent(e)
|
|
case key.EditEvent:
|
|
io.WriteString(&mw.textbuffer, e.Text)
|
|
|
|
case key.Event:
|
|
switch e.Name {
|
|
case key.NameEnter, key.NameReturn:
|
|
io.WriteString(&mw.textbuffer, "\n")
|
|
}
|
|
mw.ctx.Input.Keyboard.Keys = append(mw.ctx.Input.Keyboard.Keys, gio2mobileKey(e))
|
|
}
|
|
}
|
|
|
|
mw.ctx.Windows[0].Bounds = rect.Rect{X: 0, Y: 0, W: mw.size.X, H: mw.size.Y}
|
|
in := &mw.ctx.Input
|
|
in.Mouse.clip = nk_null_rect
|
|
in.Keyboard.Text = mw.textbuffer.String()
|
|
mw.textbuffer.Reset()
|
|
|
|
var t0, t1, te time.Time
|
|
if perfUpdate || mw.Perf {
|
|
t0 = time.Now()
|
|
}
|
|
|
|
if dumpFrame && !perfUpdate {
|
|
panic("dumpFrame")
|
|
}
|
|
|
|
mw.ctx.Update()
|
|
|
|
if perfUpdate || mw.Perf {
|
|
t1 = time.Now()
|
|
}
|
|
nprimitives := mw.draw()
|
|
if perfUpdate && nprimitives > 0 {
|
|
te = time.Now()
|
|
|
|
fps := 1.0 / te.Sub(t0).Seconds()
|
|
|
|
fmt.Printf("Update %0.4f msec = %0.4f updatefn + %0.4f draw (%d primitives) [max fps %0.2f]\n", te.Sub(t0).Seconds()*1000, t1.Sub(t0).Seconds()*1000, te.Sub(t1).Seconds()*1000, nprimitives, fps)
|
|
}
|
|
if mw.Perf && nprimitives > 0 {
|
|
te = time.Now()
|
|
fps := 1.0 / te.Sub(t0).Seconds()
|
|
|
|
s := fmt.Sprintf("%0.4fms + %0.4fms (%0.2f)\n%s", t1.Sub(t0).Seconds()*1000, te.Sub(t1).Seconds()*1000, fps, perfString)
|
|
|
|
font := mw.Style().Font
|
|
txt := fontFace2fontFace(&font).layout(s, -1)
|
|
|
|
bounds := image.Point{X: maxLinesWidth(txt), Y: (txt[0].Ascent + txt[0].Descent).Ceil() * 2}
|
|
|
|
pos := mw.size
|
|
pos.Y -= bounds.Y
|
|
pos.X -= bounds.X
|
|
|
|
paintRect := f32.Rectangle{f32.Point{float32(pos.X), float32(pos.Y)}, f32.Point{float32(pos.X + bounds.X), float32(pos.Y + bounds.Y)}}
|
|
|
|
var stack op.StackOp
|
|
stack.Push(&mw.ops)
|
|
paint.ColorOp{Color: color.RGBA{0xff, 0xff, 0xff, 0xff}}.Add(&mw.ops)
|
|
paint.PaintOp{Rect: paintRect}.Add(&mw.ops)
|
|
stack.Pop()
|
|
|
|
drawText(&mw.ops, txt, font, color.RGBA{0x00, 0x00, 0x00, 0xff}, pos, bounds, paintRect)
|
|
}
|
|
}
|
|
|
|
func (w *masterWindow) draw() int {
|
|
if !w.drawChanged() {
|
|
return 0
|
|
}
|
|
|
|
w.prevCmds = append(w.prevCmds[:0], w.ctx.cmds...)
|
|
|
|
return w.ctx.Draw(&w.ops, w.size, w.Perf)
|
|
}
|
|
|
|
func (ctx *context) Draw(ops *op.Ops, size image.Point, perf bool) int {
|
|
ops.Reset()
|
|
|
|
if perf {
|
|
profile.Op{ctx}.Add(ops)
|
|
}
|
|
pointer.InputOp{ctx, false}.Add(ops)
|
|
key.InputOp{ctx, true}.Add(ops)
|
|
|
|
var scissorStack op.StackOp
|
|
scissorless := true
|
|
|
|
for i := range ctx.cmds {
|
|
icmd := &ctx.cmds[i]
|
|
switch icmd.Kind {
|
|
case command.ScissorCmd:
|
|
if !scissorless {
|
|
scissorStack.Pop()
|
|
}
|
|
scissorStack.Push(ops)
|
|
gioclip.Rect{Rect: n2fRect(icmd.Rect)}.Op(ops).Add(ops)
|
|
scissorless = false
|
|
|
|
case command.LineCmd:
|
|
cmd := icmd.Line
|
|
|
|
var stack op.StackOp
|
|
stack.Push(ops)
|
|
paint.ColorOp{Color: cmd.Color}.Add(ops)
|
|
|
|
h1 := int(cmd.LineThickness / 2)
|
|
h2 := int(cmd.LineThickness) - h1
|
|
|
|
if cmd.Begin.X == cmd.End.X {
|
|
y0, y1 := cmd.Begin.Y, cmd.End.Y
|
|
if y0 > y1 {
|
|
y0, y1 = y1, y0
|
|
}
|
|
paint.PaintOp{Rect: f32.Rectangle{
|
|
f32.Point{float32(cmd.Begin.X - h1), float32(y0)},
|
|
f32.Point{float32(cmd.Begin.X + h2), float32(y1)}}}.Add(ops)
|
|
} else if cmd.Begin.Y == cmd.End.Y {
|
|
x0, x1 := cmd.Begin.X, cmd.End.X
|
|
if x0 > x1 {
|
|
x0, x1 = x1, x0
|
|
}
|
|
paint.PaintOp{Rect: f32.Rectangle{
|
|
f32.Point{float32(x0), float32(cmd.Begin.Y - h1)},
|
|
f32.Point{float32(x1), float32(cmd.Begin.Y + h2)}}}.Add(ops)
|
|
} else {
|
|
m := float32(cmd.Begin.Y-cmd.End.Y) / float32(cmd.Begin.X-cmd.End.X)
|
|
invm := -1 / m
|
|
|
|
xadv := float32(math.Sqrt(float64(cmd.LineThickness*cmd.LineThickness) / (4 * float64((invm*invm + 1)))))
|
|
yadv := xadv * invm
|
|
|
|
var p gioclip.Path
|
|
p.Begin(ops)
|
|
|
|
pa := f32.Point{float32(cmd.Begin.X) - xadv, float32(cmd.Begin.Y) - yadv}
|
|
p.Move(pa)
|
|
pb := f32.Point{2 * xadv, 2 * yadv}
|
|
p.Line(pb)
|
|
pc := f32.Point{float32(cmd.End.X - cmd.Begin.X), float32(cmd.End.Y - cmd.Begin.Y)}
|
|
p.Line(pc)
|
|
pd := f32.Point{-2 * xadv, -2 * yadv}
|
|
p.Line(pd)
|
|
p.Line(f32.Point{float32(cmd.Begin.X - cmd.End.X), float32(cmd.Begin.Y - cmd.End.Y)})
|
|
|
|
p.End().Add(ops)
|
|
|
|
pb = pb.Add(pa)
|
|
pc = pc.Add(pb)
|
|
pd = pd.Add(pc)
|
|
|
|
minp := f32.Point{
|
|
min4(pa.X, pb.X, pc.X, pd.X),
|
|
min4(pa.Y, pb.Y, pc.Y, pd.Y)}
|
|
maxp := f32.Point{
|
|
max4(pa.X, pb.X, pc.X, pd.X),
|
|
max4(pa.Y, pb.Y, pc.Y, pd.Y)}
|
|
|
|
paint.PaintOp{Rect: f32.Rectangle{minp, maxp}}.Add(ops)
|
|
}
|
|
|
|
stack.Pop()
|
|
|
|
case command.RectFilledCmd:
|
|
cmd := icmd.RectFilled
|
|
// rounding is true if rounding has been requested AND we can draw it
|
|
rounding := cmd.Rounding > 0 && int(cmd.Rounding*2) < icmd.W && int(cmd.Rounding*2) < icmd.H
|
|
|
|
var stack op.StackOp
|
|
stack.Push(ops)
|
|
paint.ColorOp{Color: cmd.Color}.Add(ops)
|
|
|
|
if rounding {
|
|
const c = 0.55228475 // 4*(sqrt(2)-1)/3
|
|
|
|
x, y, w, h := float32(icmd.X), float32(icmd.Y), float32(icmd.W), float32(icmd.H)
|
|
r := float32(cmd.Rounding)
|
|
|
|
var b gioclip.Path
|
|
b.Begin(ops)
|
|
b.Move(f32.Point{X: x + w, Y: y + h - r})
|
|
b.Cube(f32.Point{X: 0, Y: r * c}, f32.Point{X: -r + r*c, Y: r}, f32.Point{X: -r, Y: r}) // SE
|
|
b.Line(f32.Point{X: r - w + r, Y: 0})
|
|
b.Cube(f32.Point{X: -r * c, Y: 0}, f32.Point{X: -r, Y: -r + r*c}, f32.Point{X: -r, Y: -r}) // SW
|
|
b.Line(f32.Point{X: 0, Y: r - h + r})
|
|
b.Cube(f32.Point{X: 0, Y: -r * c}, f32.Point{X: r - r*c, Y: -r}, f32.Point{X: r, Y: -r}) // NW
|
|
b.Line(f32.Point{X: w - r - r, Y: 0})
|
|
b.Cube(f32.Point{X: r * c, Y: 0}, f32.Point{X: r, Y: r - r*c}, f32.Point{X: r, Y: r}) // NE
|
|
b.End().Add(ops)
|
|
}
|
|
|
|
paint.PaintOp{Rect: n2fRect(icmd.Rect)}.Add(ops)
|
|
stack.Pop()
|
|
|
|
case command.TriangleFilledCmd:
|
|
cmd := icmd.TriangleFilled
|
|
|
|
var stack op.StackOp
|
|
stack.Push(ops)
|
|
|
|
paint.ColorOp{cmd.Color}.Add(ops)
|
|
|
|
var p gioclip.Path
|
|
p.Begin(ops)
|
|
p.Move(f32.Point{float32(cmd.A.X), float32(cmd.A.Y)})
|
|
p.Line(f32.Point{float32(cmd.B.X - cmd.A.X), float32(cmd.B.Y - cmd.A.Y)})
|
|
p.Line(f32.Point{float32(cmd.C.X - cmd.B.X), float32(cmd.C.Y - cmd.B.Y)})
|
|
p.Line(f32.Point{float32(cmd.A.X - cmd.C.X), float32(cmd.A.Y - cmd.C.Y)})
|
|
p.End().Add(ops)
|
|
|
|
pmin := f32.Point{
|
|
min2(min2(float32(cmd.A.X), float32(cmd.B.X)), float32(cmd.C.X)),
|
|
min2(min2(float32(cmd.A.Y), float32(cmd.B.Y)), float32(cmd.C.Y))}
|
|
|
|
pmax := f32.Point{
|
|
max2(max2(float32(cmd.A.X), float32(cmd.B.X)), float32(cmd.C.X)),
|
|
max2(max2(float32(cmd.A.Y), float32(cmd.B.Y)), float32(cmd.C.Y))}
|
|
|
|
paint.PaintOp{Rect: f32.Rectangle{pmin, pmax}}.Add(ops)
|
|
|
|
stack.Pop()
|
|
|
|
case command.CircleFilledCmd:
|
|
var stack op.StackOp
|
|
stack.Push(ops)
|
|
|
|
paint.ColorOp{icmd.CircleFilled.Color}.Add(ops)
|
|
|
|
r := min2(float32(icmd.W), float32(icmd.H)) / 2
|
|
|
|
const c = 0.55228475 // 4*(sqrt(2)-1)/3
|
|
var b gioclip.Path
|
|
b.Begin(ops)
|
|
b.Move(f32.Point{X: float32(icmd.X) + r*2, Y: float32(icmd.Y) + r})
|
|
b.Cube(f32.Point{X: 0, Y: r * c}, f32.Point{X: -r + r*c, Y: r}, f32.Point{X: -r, Y: r}) // SE
|
|
b.Cube(f32.Point{X: -r * c, Y: 0}, f32.Point{X: -r, Y: -r + r*c}, f32.Point{X: -r, Y: -r}) // SW
|
|
b.Cube(f32.Point{X: 0, Y: -r * c}, f32.Point{X: r - r*c, Y: -r}, f32.Point{X: r, Y: -r}) // NW
|
|
b.Cube(f32.Point{X: r * c, Y: 0}, f32.Point{X: r, Y: r - r*c}, f32.Point{X: r, Y: r}) // NE
|
|
b.End().Add(ops)
|
|
|
|
paint.PaintOp{Rect: n2fRect(icmd.Rect)}.Add(ops)
|
|
|
|
stack.Pop()
|
|
|
|
case command.ImageCmd:
|
|
var stack op.StackOp
|
|
stack.Push(ops)
|
|
//TODO: this should be retained between frames somehow...
|
|
paint.NewImageOp(icmd.Image.Img).Add(ops)
|
|
paint.PaintOp{n2fRect(icmd.Rect)}.Add(ops)
|
|
stack.Pop()
|
|
|
|
case command.TextCmd:
|
|
txt := fontFace2fontFace(&icmd.Text.Face).layout(icmd.Text.String, -1)
|
|
if len(txt) <= 0 {
|
|
continue
|
|
}
|
|
|
|
bounds := image.Point{X: maxLinesWidth(txt), Y: (txt[0].Ascent + txt[0].Descent).Ceil()}
|
|
if bounds.X > icmd.W {
|
|
bounds.X = icmd.W
|
|
}
|
|
if bounds.Y > icmd.H {
|
|
bounds.Y = icmd.H
|
|
}
|
|
|
|
drawText(ops, txt, icmd.Text.Face, icmd.Text.Foreground, image.Point{icmd.X, icmd.Y}, bounds, n2fRect(icmd.Rect))
|
|
|
|
default:
|
|
panic(UnknownCommandErr)
|
|
}
|
|
}
|
|
|
|
return len(ctx.cmds)
|
|
}
|
|
|
|
func n2fRect(r rect.Rect) f32.Rectangle {
|
|
return f32.Rectangle{
|
|
Min: f32.Point{float32(r.X), float32(r.Y)},
|
|
Max: f32.Point{float32(r.X + r.W), float32(r.Y + r.H)}}
|
|
}
|
|
|
|
func i2fRect(r image.Rectangle) f32.Rectangle {
|
|
return f32.Rectangle{
|
|
Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)},
|
|
Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)}}
|
|
}
|
|
|
|
func min4(a, b, c, d float32) float32 {
|
|
return min2(min2(a, b), min2(c, d))
|
|
}
|
|
|
|
func min2(a, b float32) float32 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func max4(a, b, c, d float32) float32 {
|
|
return max2(max2(a, b), max2(c, d))
|
|
}
|
|
|
|
func max2(a, b float32) float32 {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func textPadding(lines []text.Line) (padding image.Rectangle) {
|
|
if len(lines) == 0 {
|
|
return
|
|
}
|
|
first := lines[0]
|
|
if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
|
|
padding.Min.Y = d.Ceil()
|
|
}
|
|
last := lines[len(lines)-1]
|
|
if d := last.Bounds.Max.Y - last.Descent; d > 0 {
|
|
padding.Max.Y = d.Ceil()
|
|
}
|
|
if d := first.Bounds.Min.X; d < 0 {
|
|
padding.Min.X = d.Ceil()
|
|
}
|
|
if d := first.Bounds.Max.X - first.Width; d > 0 {
|
|
padding.Max.X = d.Ceil()
|
|
}
|
|
return
|
|
}
|
|
|
|
func clipLine(line text.Line, clip image.Rectangle) (text.Line, f32.Point, bool) {
|
|
off := fixed.Point26_6{X: fixed.I(0), Y: fixed.I(line.Ascent.Ceil())}
|
|
for len(line.Layout) > 0 {
|
|
adv := line.Layout[0].Advance
|
|
if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= clip.Min.X {
|
|
break
|
|
}
|
|
off.X += adv
|
|
line.Layout = line.Layout[1:]
|
|
}
|
|
endx := off.X
|
|
for i, glyph := range line.Layout {
|
|
if (endx + line.Bounds.Min.X).Floor() > clip.Max.X {
|
|
line.Layout = line.Layout[:i]
|
|
break
|
|
}
|
|
endx += glyph.Advance
|
|
}
|
|
offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64}
|
|
return line, offf, true
|
|
}
|
|
|
|
func maxLinesWidth(lines []text.Line) int {
|
|
w := 0
|
|
for _, line := range lines {
|
|
if line.Width.Ceil() > w {
|
|
w = line.Width.Ceil()
|
|
}
|
|
}
|
|
return w
|
|
}
|
|
|
|
func drawText(ops *op.Ops, txt []text.Line, face font.Face, fgcolor color.RGBA, pos, bounds image.Point, paintRect f32.Rectangle) {
|
|
clip := textPadding(txt)
|
|
clip.Max = clip.Max.Add(bounds)
|
|
|
|
var stack op.StackOp
|
|
stack.Push(ops)
|
|
paint.ColorOp{fgcolor}.Add(ops)
|
|
|
|
fc := fontFace2fontFace(&face)
|
|
|
|
for i := range txt {
|
|
txtstr, off, ok := clipLine(txt[i], clip)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
off.X += float32(pos.X)
|
|
off.Y += float32(pos.Y) + float32(i*FontHeight(face))
|
|
|
|
var stack op.StackOp
|
|
stack.Push(ops)
|
|
|
|
op.TransformOp{}.Offset(off).Add(ops)
|
|
fc.shape(txtstr.Layout).Add(ops)
|
|
|
|
paint.PaintOp{Rect: paintRect.Sub(off)}.Add(ops)
|
|
|
|
stack.Pop()
|
|
}
|
|
|
|
stack.Pop()
|
|
}
|
|
|
|
type fontFace struct {
|
|
fnt *opentype.Font
|
|
shaper *text.FontRegistry
|
|
size int
|
|
fsize fixed.Int26_6
|
|
metrics ifont.Metrics
|
|
}
|
|
|
|
func fontFace2fontFace(f *font.Face) *fontFace {
|
|
return (*fontFace)(unsafe.Pointer(f))
|
|
}
|
|
|
|
func (face *fontFace) layout(str string, width int) []text.Line {
|
|
if width < 0 {
|
|
width = 1e6
|
|
}
|
|
return face.shaper.LayoutString(text.Font{}, fixed.I(face.size), width, str)
|
|
}
|
|
|
|
func (face *fontFace) shape(txtstr []text.Glyph) op.CallOp {
|
|
return face.shaper.Shape(text.Font{}, fixed.I(face.size), txtstr)
|
|
}
|
|
|
|
func (face *fontFace) Px(v unit.Value) int {
|
|
return face.size
|
|
}
|
|
|
|
func ChangeFontWidthCache(size int) {
|
|
}
|
|
|
|
func FontWidth(f font.Face, str string) int {
|
|
txt := fontFace2fontFace(&f).layout(str, -1)
|
|
maxw := 0
|
|
for i := range txt {
|
|
if w := txt[i].Width.Ceil(); w > maxw {
|
|
maxw = w
|
|
}
|
|
}
|
|
return maxw
|
|
}
|
|
|
|
func glyphAdvance(f font.Face, ch rune) int {
|
|
txt := fontFace2fontFace(&f).layout(string(ch), 1e6)
|
|
return txt[0].Width.Ceil()
|
|
}
|
|
|
|
func measureRunes(f font.Face, runes []rune) int {
|
|
text := fontFace2fontFace(&f).layout(string(runes), 1e6)
|
|
w := fixed.Int26_6(0)
|
|
for i := range text {
|
|
w += text[i].Width
|
|
}
|
|
return w.Ceil()
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
// TEXT WIDGETS
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
const (
|
|
tabSizeInSpaces = 8
|
|
)
|
|
|
|
type textWidget struct {
|
|
Padding image.Point
|
|
Background color.RGBA
|
|
Text color.RGBA
|
|
}
|
|
|
|
func widgetText(o *command.Buffer, b rect.Rect, str string, t *textWidget, a label.Align, f font.Face) {
|
|
b.H = max(b.H, 2*t.Padding.Y)
|
|
lblrect := rect.Rect{X: 0, W: 0, Y: b.Y + t.Padding.Y, H: b.H - 2*t.Padding.Y}
|
|
|
|
/* align in x-axis */
|
|
switch a[0] {
|
|
case 'L':
|
|
lblrect.X = b.X + t.Padding.X
|
|
lblrect.W = max(0, b.W-2*t.Padding.X)
|
|
case 'C':
|
|
text_width := FontWidth(f, str)
|
|
text_width += (2.0 * t.Padding.X)
|
|
lblrect.W = max(1, 2*t.Padding.X+text_width)
|
|
lblrect.X = (b.X + t.Padding.X + ((b.W-2*t.Padding.X)-lblrect.W)/2)
|
|
lblrect.X = max(b.X+t.Padding.X, lblrect.X)
|
|
lblrect.W = min(b.X+b.W, lblrect.X+lblrect.W)
|
|
if lblrect.W >= lblrect.X {
|
|
lblrect.W -= lblrect.X
|
|
}
|
|
case 'R':
|
|
text_width := FontWidth(f, str)
|
|
text_width += (2.0 * t.Padding.X)
|
|
lblrect.X = max(b.X+t.Padding.X, (b.X+b.W)-(2*t.Padding.X+text_width))
|
|
lblrect.W = text_width + 2*t.Padding.X
|
|
default:
|
|
panic("unsupported alignment")
|
|
}
|
|
|
|
/* align in y-axis */
|
|
if len(a) >= 2 {
|
|
switch a[1] {
|
|
case 'C':
|
|
lblrect.Y = b.Y + b.H/2.0 - FontHeight(f)/2.0
|
|
case 'B':
|
|
lblrect.Y = b.Y + b.H - FontHeight(f)
|
|
}
|
|
}
|
|
if lblrect.H < FontHeight(f)*2 {
|
|
lblrect.H = FontHeight(f) * 2
|
|
}
|
|
|
|
o.DrawText(lblrect, str, f, t.Text)
|
|
}
|
|
|
|
func widgetTextWrap(o *command.Buffer, b rect.Rect, str []rune, t *textWidget, f font.Face) {
|
|
var text textWidget
|
|
|
|
text.Padding = image.Point{0, 0}
|
|
text.Background = t.Background
|
|
text.Text = t.Text
|
|
|
|
b.W = max(b.W, 2*t.Padding.X)
|
|
b.H = max(b.H, 2*t.Padding.Y)
|
|
b.H = b.H - 2*t.Padding.Y
|
|
|
|
var line rect.Rect
|
|
line.X = b.X + t.Padding.X
|
|
line.Y = b.Y + t.Padding.Y
|
|
line.W = b.W - 2*t.Padding.X
|
|
line.H = 2*t.Padding.Y + FontHeight(f)
|
|
|
|
lines := fontFace2fontFace(&f).layout(string(str), line.W)
|
|
|
|
for _, txtline := range lines {
|
|
if line.Y+line.H >= (b.Y + b.H) {
|
|
break
|
|
}
|
|
runes := make([]rune, len(txtline.Layout))
|
|
for i := range txtline.Layout {
|
|
runes[i] = txtline.Layout[i].Rune
|
|
}
|
|
widgetText(o, line, string(runes), &text, "LC", f)
|
|
line.Y += FontHeight(f) + 2*t.Padding.Y
|
|
}
|
|
}
|