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.
NoiseTorch/vendor/github.com/aarzilli/nucular/shiny.go

974 lines
24 KiB
Go

// +build !darwin,!nucular_gio nucular_shiny
package nucular
import (
"bytes"
"fmt"
"image"
"image/color"
"image/draw"
"math"
"os"
"strings"
"sync"
"sync/atomic"
"time"
"unsafe"
"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"
"golang.org/x/exp/shiny/driver"
"golang.org/x/exp/shiny/screen"
"golang.org/x/mobile/event/key"
"golang.org/x/mobile/event/lifecycle"
"golang.org/x/mobile/event/mouse"
"golang.org/x/mobile/event/paint"
"golang.org/x/mobile/event/size"
"github.com/golang/freetype/raster"
ifont "golang.org/x/image/font"
"golang.org/x/image/math/fixed"
"github.com/hashicorp/golang-lru"
)
//go:generate go-bindata -o internal/assets/assets.go -pkg assets DroidSansMono.ttf
var clipboardStarted bool = false
var clipboardMu sync.Mutex
type masterWindow struct {
masterWindowCommon
Title string
screen screen.Screen
wnd screen.Window
wndb screen.Buffer
bounds image.Rectangle
initialSize image.Point
// window is focused
Focus bool
textbuffer bytes.Buffer
closing bool
focusedOnce bool
}
// Creates new master window
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
}
// Shows window, runs event loop
func (mw *masterWindow) Main() {
driver.Main(mw.main)
}
func (mw *masterWindow) Lock() {
mw.uilock.Lock()
}
func (mw *masterWindow) Unlock() {
mw.uilock.Unlock()
}
func (mw *masterWindow) main(s screen.Screen) {
var err error
mw.screen = s
width, height := mw.ctx.scale(mw.initialSize.X), mw.ctx.scale(mw.initialSize.Y)
mw.wnd, err = s.NewWindow(&screen.NewWindowOptions{width, height, mw.Title})
if err != nil {
fmt.Fprintf(os.Stderr, "could not create window: %v", err)
return
}
mw.setupBuffer(image.Point{width, height})
mw.Changed()
go mw.updater()
for {
ei := mw.wnd.NextEvent()
mw.uilock.Lock()
r := mw.handleEventLocked(ei)
mw.uilock.Unlock()
if !r {
break
}
}
}
func (w *masterWindow) handleEventLocked(ei interface{}) bool {
switch e := ei.(type) {
case paint.Event:
// On darwin we must respond to a paint.Event by reuploading the buffer or
// the appplication will freeze.
// On windows when the window goes off screen part of the window contents
// will be discarded and must be redrawn.
w.prevCmds = w.prevCmds[:0]
w.updateLocked()
case lifecycle.Event:
if e.Crosses(lifecycle.StageDead) == lifecycle.CrossOn || e.To == lifecycle.StageDead || w.closing {
w.closing = true
w.closeLocked()
return false
}
c := false
switch cross := e.Crosses(lifecycle.StageFocused); cross {
case lifecycle.CrossOn:
if !w.focusedOnce {
// on linux uploads that happen before this event don't get displayed
// for some reason, force a reupload
w.focusedOnce = true
w.prevCmds = w.prevCmds[:0]
}
w.Focus = true
c = true
case lifecycle.CrossOff:
w.Focus = false
c = true
}
if c {
if changed := atomic.LoadInt32(&w.ctx.changed); changed < 2 {
atomic.StoreInt32(&w.ctx.changed, 2)
}
}
case size.Event:
sz := e.Size()
bb := w.wndb.Bounds()
if sz.X <= bb.Dx() && sz.Y <= bb.Dy() {
w.bounds = w.wndb.Bounds()
w.bounds.Max.Y = w.bounds.Min.Y + sz.Y
w.bounds.Max.X = w.bounds.Min.X + sz.X
} else {
if w.wndb != nil {
w.wndb.Release()
}
w.setupBuffer(sz)
}
w.prevCmds = w.prevCmds[:0]
if changed := atomic.LoadInt32(&w.ctx.changed); changed < 2 {
atomic.StoreInt32(&w.ctx.changed, 2)
}
case mouse.Event:
changed := atomic.LoadInt32(&w.ctx.changed)
if changed < 2 {
atomic.StoreInt32(&w.ctx.changed, 2)
}
switch e.Direction {
case mouse.DirStep:
switch e.Button {
case mouse.ButtonWheelUp:
w.ctx.Input.Mouse.ScrollDelta++
case mouse.ButtonWheelDown:
w.ctx.Input.Mouse.ScrollDelta--
}
case mouse.DirPress, mouse.DirRelease:
down := e.Direction == mouse.DirPress
if e.Button >= 0 && int(e.Button) < len(w.ctx.Input.Mouse.Buttons) {
btn := &w.ctx.Input.Mouse.Buttons[e.Button]
if btn.Down == down {
break
}
if down {
btn.ClickedPos.X = int(e.X)
btn.ClickedPos.Y = int(e.Y)
}
btn.Clicked = true
btn.Down = down
}
case mouse.DirNone:
w.ctx.Input.Mouse.Pos.X = int(e.X)
w.ctx.Input.Mouse.Pos.Y = int(e.Y)
w.ctx.Input.Mouse.Delta = w.ctx.Input.Mouse.Pos.Sub(w.ctx.Input.Mouse.Prev)
}
case key.Event:
changed := atomic.LoadInt32(&w.ctx.changed)
if changed < 2 {
atomic.StoreInt32(&w.ctx.changed, 2)
}
w.ctx.processKeyEvent(e, &w.textbuffer)
}
return true
}
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.closing {
return
}
changed := atomic.LoadInt32(&w.ctx.changed)
if changed > 0 {
atomic.AddInt32(&w.ctx.changed, -1)
w.updateLocked()
} else {
down = false
for _, btn := range w.ctx.Input.Mouse.Buttons {
if btn.Down {
down = true
}
}
if down {
w.updateLocked()
}
}
}()
}
}
func (w *masterWindow) updateLocked() {
w.ctx.Windows[0].Bounds = rect.FromRectangle(w.bounds)
in := &w.ctx.Input
in.Mouse.clip = nk_null_rect
in.Keyboard.Text = w.textbuffer.String()
w.textbuffer.Reset()
var t0, t1, te time.Time
if perfUpdate || w.Perf {
t0 = time.Now()
}
if dumpFrame && !perfUpdate {
panic("dumpFrame")
}
w.ctx.Update()
if perfUpdate || w.Perf {
t1 = time.Now()
}
nprimitives := w.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 w.Perf && nprimitives > 0 {
te = time.Now()
img := w.wndb.RGBA()
bounds := w.bounds
fps := 1.0 / te.Sub(t0).Seconds()
s := fmt.Sprintf("%0.4fms + %0.4fms (%0.2f)", t1.Sub(t0).Seconds()*1000, te.Sub(t1).Seconds()*1000, fps)
d := ifont.Drawer{
Dst: img,
Src: image.White,
Face: fontFace2fontFace(&w.ctx.Style.Font).face}
width := d.MeasureString(s).Ceil()
bounds.Min.X = bounds.Max.X - width
bounds.Min.Y = bounds.Max.Y - (w.ctx.Style.Font.Metrics().Ascent + w.ctx.Style.Font.Metrics().Descent).Ceil()
draw.Draw(img, bounds, image.Black, bounds.Min, draw.Src)
d.Dot = fixed.P(bounds.Min.X, bounds.Min.Y+w.ctx.Style.Font.Metrics().Ascent.Ceil())
d.DrawString(s)
}
if dumpFrame && frameCnt < 1000 && nprimitives > 0 {
w.dumpFrame(w.wndb.RGBA(), t0, t1, te, nprimitives)
}
if nprimitives > 0 {
w.wnd.Upload(w.bounds.Min, w.wndb, w.bounds)
w.wnd.Publish()
}
}
func (w *masterWindow) closeLocked() {
w.closing = true
if w.wndb != nil {
w.wndb.Release()
}
w.wnd.Release()
}
// Programmatically closes window.
func (mw *masterWindow) Close() {
mw.wnd.Send(lifecycle.Event{From: lifecycle.StageAlive, To: lifecycle.StageDead})
}
// Returns true if the window is closed.
func (mw *masterWindow) Closed() bool {
mw.uilock.Lock()
defer mw.uilock.Unlock()
return mw.closing
}
func (w *masterWindow) setupBuffer(sz image.Point) {
var err error
oldb := w.wndb
w.wndb, err = w.screen.NewBuffer(sz)
if err != nil {
fmt.Fprintf(os.Stderr, "could not setup buffer: %v", err)
w.wndb = oldb
}
w.bounds = w.wndb.Bounds()
}
func (w *masterWindow) draw() int {
if !w.drawChanged() {
return 0
}
w.prevCmds = append(w.prevCmds[:0], w.ctx.cmds...)
return w.ctx.Draw(w.wndb.RGBA())
}
var cnt = 0
var ln, frect, frectover, brrect, frrect, ftri, circ, fcirc, txt int
func (ctx *context) Draw(wimg *image.RGBA) int {
var txttim, tritim, brecttim, frecttim, frectovertim, frrecttim time.Duration
var t0 time.Time
img := wimg
var painter *myRGBAPainter
var rasterizer *raster.Rasterizer
roundAngle := func(cx, cy int, radius uint16, startAngle, angle float64, c color.Color) {
rasterizer.Clear()
rasterizer.Start(fixed.P(cx, cy))
traceArc(rasterizer, float64(cx), float64(cy), float64(radius), float64(radius), startAngle, angle, false)
rasterizer.Add1(fixed.P(cx, cy))
painter.SetColor(c)
rasterizer.Rasterize(painter)
}
setupRasterizer := func() {
rasterizer = raster.NewRasterizer(img.Bounds().Dx(), img.Bounds().Dy())
painter = &myRGBAPainter{Image: img}
}
if ctx.cmdstim != nil {
ctx.cmdstim = ctx.cmdstim[:0]
}
transparentBorderOptimization := false
for i := range ctx.cmds {
if perfUpdate {
t0 = time.Now()
}
icmd := &ctx.cmds[i]
switch icmd.Kind {
case command.ScissorCmd:
img = wimg.SubImage(icmd.Rectangle()).(*image.RGBA)
painter = nil
rasterizer = nil
case command.LineCmd:
cmd := icmd.Line
colimg := image.NewUniform(cmd.Color)
op := draw.Over
if cmd.Color.A == 0xff {
op = draw.Src
}
h1 := int(cmd.LineThickness / 2)
h2 := int(cmd.LineThickness) - h1
if cmd.Begin.X == cmd.End.X {
// draw vertical line
r := image.Rect(cmd.Begin.X-h1, cmd.Begin.Y, cmd.Begin.X+h2, cmd.End.Y)
drawFill(img, r, colimg, r.Min, op)
} else if cmd.Begin.Y == cmd.End.Y {
// draw horizontal line
r := image.Rect(cmd.Begin.X, cmd.Begin.Y-h1, cmd.End.X, cmd.Begin.Y+h2)
drawFill(img, r, colimg, r.Min, op)
} else {
if rasterizer == nil {
setupRasterizer()
}
unzw := rasterizer.UseNonZeroWinding
rasterizer.UseNonZeroWinding = true
var p raster.Path
p.Start(fixed.P(cmd.Begin.X-img.Bounds().Min.X, cmd.Begin.Y-img.Bounds().Min.Y))
p.Add1(fixed.P(cmd.End.X-img.Bounds().Min.X, cmd.End.Y-img.Bounds().Min.Y))
rasterizer.Clear()
rasterizer.AddStroke(p, fixed.I(int(cmd.LineThickness)), nil, nil)
painter.SetColor(cmd.Color)
rasterizer.Rasterize(painter)
rasterizer.UseNonZeroWinding = unzw
}
ln++
case command.RectFilledCmd:
cmd := icmd.RectFilled
if i == 0 {
// first command draws the background, insure that it's always fully opaque
cmd.Color.A = 0xff
}
if transparentBorderOptimization {
transparentBorderOptimization = false
prevcmd := ctx.cmds[i-1].RectFilled
const m = 1<<16 - 1
sr, sg, sb, sa := cmd.Color.RGBA()
a := (m - sa) * 0x101
cmd.Color.R = uint8((uint32(prevcmd.Color.R)*a/m + sr) >> 8)
cmd.Color.G = uint8((uint32(prevcmd.Color.G)*a/m + sg) >> 8)
cmd.Color.B = uint8((uint32(prevcmd.Color.B)*a/m + sb) >> 8)
cmd.Color.A = uint8((uint32(prevcmd.Color.A)*a/m + sa) >> 8)
}
colimg := image.NewUniform(cmd.Color)
op := draw.Over
if cmd.Color.A == 0xff {
op = draw.Src
}
body := icmd.Rectangle()
var lwing, rwing image.Rectangle
// 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
if rounding {
body.Min.X += int(cmd.Rounding)
body.Max.X -= int(cmd.Rounding)
lwing = image.Rect(icmd.X, icmd.Y+int(cmd.Rounding), icmd.X+int(cmd.Rounding), icmd.Y+icmd.H-int(cmd.Rounding))
rwing = image.Rect(icmd.X+icmd.W-int(cmd.Rounding), lwing.Min.Y, icmd.X+icmd.W, lwing.Max.Y)
}
bordopt := false
if ok, border := borderOptimize(icmd, ctx.cmds, i+1); ok {
// only draw parts of body if this command can be optimized to a border with the next command
bordopt = true
if ctx.cmds[i+1].RectFilled.Color.A != 0xff {
transparentBorderOptimization = true
}
border += int(ctx.cmds[i+1].RectFilled.Rounding)
top := image.Rect(body.Min.X, body.Min.Y, body.Max.X, body.Min.Y+border)
bot := image.Rect(body.Min.X, body.Max.Y-border, body.Max.X, body.Max.Y)
drawFill(img, top, colimg, top.Min, op)
drawFill(img, bot, colimg, bot.Min, op)
if border < int(cmd.Rounding) {
// wings need shrinking
d := int(cmd.Rounding) - border
lwing.Max.Y -= d
rwing.Min.Y += d
} else {
// display extra wings
d := border - int(cmd.Rounding)
xlwing := image.Rect(top.Min.X, top.Max.Y, top.Min.X+d, bot.Min.Y)
xrwing := image.Rect(top.Max.X-d, top.Max.Y, top.Max.X, bot.Min.Y)
drawFill(img, xlwing, colimg, xlwing.Min, op)
drawFill(img, xrwing, colimg, xrwing.Min, op)
}
brrect++
} else {
drawFill(img, body, colimg, body.Min, op)
if cmd.Rounding == 0 {
if op == draw.Src {
frect++
} else {
frectover++
}
} else {
frrect++
}
}
if rounding {
drawFill(img, lwing, colimg, lwing.Min, op)
drawFill(img, rwing, colimg, rwing.Min, op)
rangle := math.Pi / 2
if rasterizer == nil {
setupRasterizer()
}
minx := img.Bounds().Min.X
miny := img.Bounds().Min.Y
roundAngle(icmd.X+icmd.W-int(cmd.Rounding)-minx, icmd.Y+int(cmd.Rounding)-miny, cmd.Rounding, -math.Pi/2, rangle, cmd.Color)
roundAngle(icmd.X+icmd.W-int(cmd.Rounding)-minx, icmd.Y+icmd.H-int(cmd.Rounding)-miny, cmd.Rounding, 0, rangle, cmd.Color)
roundAngle(icmd.X+int(cmd.Rounding)-minx, icmd.Y+icmd.H-int(cmd.Rounding)-miny, cmd.Rounding, math.Pi/2, rangle, cmd.Color)
roundAngle(icmd.X+int(cmd.Rounding)-minx, icmd.Y+int(cmd.Rounding)-miny, cmd.Rounding, math.Pi, rangle, cmd.Color)
}
if perfUpdate {
if bordopt {
brecttim += time.Since(t0)
} else {
if cmd.Rounding > 0 {
frrecttim += time.Since(t0)
} else {
d := time.Since(t0)
if op == draw.Src {
frecttim += d
} else {
if d > 8*time.Millisecond {
fmt.Printf("outstanding rect")
}
frectovertim += d
}
}
}
}
case command.TriangleFilledCmd:
cmd := icmd.TriangleFilled
if rasterizer == nil {
setupRasterizer()
}
minx := img.Bounds().Min.X
miny := img.Bounds().Min.Y
rasterizer.Clear()
rasterizer.Start(fixed.P(cmd.A.X-minx, cmd.A.Y-miny))
rasterizer.Add1(fixed.P(cmd.B.X-minx, cmd.B.Y-miny))
rasterizer.Add1(fixed.P(cmd.C.X-minx, cmd.C.Y-miny))
rasterizer.Add1(fixed.P(cmd.A.X-minx, cmd.A.Y-miny))
painter.SetColor(cmd.Color)
rasterizer.Rasterize(painter)
ftri++
if perfUpdate {
tritim += time.Since(t0)
}
case command.CircleFilledCmd:
if rasterizer == nil {
setupRasterizer()
}
rasterizer.Clear()
startp := traceArc(rasterizer, float64(icmd.X-img.Bounds().Min.X)+float64(icmd.W/2), float64(icmd.Y-img.Bounds().Min.Y)+float64(icmd.H/2), float64(icmd.W/2), float64(icmd.H/2), 0, -math.Pi*2, true)
rasterizer.Add1(startp) // closes path
painter.SetColor(icmd.CircleFilled.Color)
rasterizer.Rasterize(painter)
fcirc++
case command.ImageCmd:
draw.Draw(img, icmd.Rectangle(), icmd.Image.Img, image.Point{}, draw.Src)
case command.TextCmd:
dstimg := wimg.SubImage(img.Bounds().Intersect(icmd.Rectangle())).(*image.RGBA)
d := ifont.Drawer{
Dst: dstimg,
Src: image.NewUniform(icmd.Text.Foreground),
Face: fontFace2fontFace(&icmd.Text.Face).face,
Dot: fixed.P(icmd.X, icmd.Y+icmd.Text.Face.Metrics().Ascent.Ceil())}
start := 0
for i := range icmd.Text.String {
if icmd.Text.String[i] == '\n' {
d.DrawString(icmd.Text.String[start:i])
d.Dot.X = fixed.I(icmd.X)
d.Dot.Y += fixed.I(FontHeight(icmd.Text.Face))
start = i + 1
}
}
if start < len(icmd.Text.String) {
d.DrawString(icmd.Text.String[start:])
}
txt++
if perfUpdate {
txttim += time.Since(t0)
}
default:
panic(UnknownCommandErr)
}
if dumpFrame {
ctx.cmdstim = append(ctx.cmdstim, time.Since(t0))
}
}
if perfUpdate {
fmt.Printf("triangle: %0.4fms text: %0.4fms brect: %0.4fms frect: %0.4fms frectover: %0.4fms frrect %0.4f\n", tritim.Seconds()*1000, txttim.Seconds()*1000, brecttim.Seconds()*1000, frecttim.Seconds()*1000, frectovertim.Seconds()*1000, frrecttim.Seconds()*1000)
}
cnt++
if perfUpdate /*&& (cnt%100) == 0*/ {
fmt.Printf("ln %d, frect %d, frectover %d, frrect %d, brrect %d, ftri %d, circ %d, fcirc %d, txt %d\n", ln, frect, frectover, frrect, brrect, ftri, circ, fcirc, txt)
ln, frect, frectover, frrect, brrect, ftri, circ, fcirc, txt = 0, 0, 0, 0, 0, 0, 0, 0, 0
}
return len(ctx.cmds)
}
// Returns true if cmds[idx] is a shrunk version of CommandFillRect and its
// color is not semitransparent and the border isn't greater than 128
func borderOptimize(cmd *command.Command, cmds []command.Command, idx int) (ok bool, border int) {
if idx >= len(cmds) {
return false, 0
}
if cmd.Kind != command.RectFilledCmd || cmds[idx].Kind != command.RectFilledCmd {
return false, 0
}
cmd2 := cmds[idx]
if cmd.RectFilled.Color.A != 0xff && cmd2.RectFilled.Color.A != 0xff {
return false, 0
}
border = cmd2.X - cmd.X
if border <= 0 || border > 128 {
return false, 0
}
if shrinkRect(cmd.Rect, border) != cmd2.Rect {
return false, 0
}
return true, border
}
func floatP(x, y float64) fixed.Point26_6 {
return fixed.Point26_6{X: fixed.Int26_6(x * 64), Y: fixed.Int26_6(y * 64)}
}
// TraceArc trace an arc using a Liner
func traceArc(t *raster.Rasterizer, x, y, rx, ry, start, angle float64, first bool) fixed.Point26_6 {
end := start + angle
clockWise := true
if angle < 0 {
clockWise = false
}
if !clockWise {
for start < end {
start += math.Pi * 2
}
end = start + angle
}
ra := (math.Abs(rx) + math.Abs(ry)) / 2
da := math.Acos(ra/(ra+0.125)) * 2
//normalize
if !clockWise {
da = -da
}
angle = start
var curX, curY float64
var startX, startY float64
for {
if (angle < end-da/4) != clockWise {
curX = x + math.Cos(end)*rx
curY = y + math.Sin(end)*ry
t.Add1(floatP(curX, curY))
return floatP(startX, startY)
}
curX = x + math.Cos(angle)*rx
curY = y + math.Sin(angle)*ry
angle += da
if first {
first = false
startX, startY = curX, curY
t.Start(floatP(curX, curY))
} else {
t.Add1(floatP(curX, curY))
}
}
}
type myRGBAPainter struct {
Image *image.RGBA
// cr, cg, cb and ca are the 16-bit color to paint the spans.
cr, cg, cb, ca uint32
}
// SetColor sets the color to paint the spans.
func (r *myRGBAPainter) SetColor(c color.Color) {
r.cr, r.cg, r.cb, r.ca = c.RGBA()
}
func (r *myRGBAPainter) Paint(ss []raster.Span, done bool) {
b := r.Image.Bounds()
cr8 := uint8(r.cr >> 8)
cg8 := uint8(r.cg >> 8)
cb8 := uint8(r.cb >> 8)
for _, s := range ss {
s.Y += b.Min.Y
s.X0 += b.Min.X
s.X1 += b.Min.X
if s.Y < b.Min.Y {
continue
}
if s.Y >= b.Max.Y {
return
}
if s.X0 < b.Min.X {
s.X0 = b.Min.X
}
if s.X1 > b.Max.X {
s.X1 = b.Max.X
}
if s.X0 >= s.X1 {
continue
}
// This code mimics drawGlyphOver in $GOROOT/src/image/draw/draw.go.
ma := s.Alpha
const m = 1<<16 - 1
i0 := (s.Y-r.Image.Rect.Min.Y)*r.Image.Stride + (s.X0-r.Image.Rect.Min.X)*4
i1 := i0 + (s.X1-s.X0)*4
if ma != m || r.ca != m {
for i := i0; i < i1; i += 4 {
dr := uint32(r.Image.Pix[i+0])
dg := uint32(r.Image.Pix[i+1])
db := uint32(r.Image.Pix[i+2])
da := uint32(r.Image.Pix[i+3])
a := (m - (r.ca * ma / m)) * 0x101
r.Image.Pix[i+0] = uint8((dr*a + r.cr*ma) / m >> 8)
r.Image.Pix[i+1] = uint8((dg*a + r.cg*ma) / m >> 8)
r.Image.Pix[i+2] = uint8((db*a + r.cb*ma) / m >> 8)
r.Image.Pix[i+3] = uint8((da*a + r.ca*ma) / m >> 8)
}
} else {
for i := i0; i < i1; i += 4 {
r.Image.Pix[i+0] = cr8
r.Image.Pix[i+1] = cg8
r.Image.Pix[i+2] = cb8
r.Image.Pix[i+3] = 0xff
}
}
}
}
// tracks github.com/aarzilli/nucular/font.Face
type fontFace struct {
face ifont.Face
}
func fontFace2fontFace(f *font.Face) *fontFace {
return (*fontFace)(unsafe.Pointer(f))
}
func textClamp(f font.Face, text []rune, space int) []rune {
text_width := 0
fc := fontFace2fontFace(&f).face
for i, ch := range text {
_, _, _, xwfixed, _ := fc.Glyph(fixed.P(0, 0), ch)
xw := xwfixed.Ceil()
if text_width+xw >= space {
return text[:i]
}
text_width += xw
}
return text
}
var fontWidthCache *lru.Cache
var fontWidthCacheSize int
func init() {
fontWidthCacheSize = 256
fontWidthCache, _ = lru.New(256)
}
func ChangeFontWidthCache(size int) {
if size > fontWidthCacheSize {
fontWidthCacheSize = size
fontWidthCache, _ = lru.New(fontWidthCacheSize)
}
}
type fontWidthCacheKey struct {
f font.Face
string string
}
func FontWidth(f font.Face, str string) int {
maxw := 0
for {
newline := strings.Index(str, "\n")
line := str
if newline >= 0 {
line = str[:newline]
}
k := fontWidthCacheKey{f, line}
var w int
if val, ok := fontWidthCache.Get(k); ok {
w = val.(int)
} else {
d := ifont.Drawer{Face: fontFace2fontFace(&f).face}
w = d.MeasureString(line).Ceil()
fontWidthCache.Add(k, w)
}
if w > maxw {
maxw = w
}
if newline >= 0 {
str = str[newline+1:]
} else {
break
}
}
return maxw
}
func glyphAdvance(f font.Face, ch rune) int {
a, _ := fontFace2fontFace(&f).face.GlyphAdvance(ch)
return a.Ceil()
}
func measureRunes(f font.Face, runes []rune) int {
var advance fixed.Int26_6
prevC := rune(-1)
fc := fontFace2fontFace(&f).face
for _, c := range runes {
if prevC >= 0 {
advance += fc.Kern(prevC, c)
}
a, ok := fc.GlyphAdvance(c)
if !ok {
// TODO: is falling back on the U+FFFD glyph the responsibility of
// the Drawer or the Face?
// TODO: set prevC = '\ufffd'?
continue
}
advance += a
prevC = c
}
return advance.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 done int = 0
var line rect.Rect
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
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)
fitting := textClamp(f, str, line.W)
for done < len(str) {
if len(fitting) == 0 || line.Y+line.H >= (b.Y+b.H) {
break
}
widgetText(o, line, string(fitting), &text, "LC", f)
done += len(fitting)
line.Y += FontHeight(f) + 2*t.Padding.Y
fitting = textClamp(f, str[done:], line.W)
}
}