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/gio.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
}
}