github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/framework/gio.go (about)

     1  package framework
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"image"
     7  	"image/color"
     8  	"io"
     9  	"math"
    10  	"os"
    11  	"sync"
    12  	"sync/atomic"
    13  	"time"
    14  	"unicode/utf8"
    15  	"unsafe"
    16  
    17  	"github.com/gop9/olt/gio/app"
    18  	"github.com/gop9/olt/gio/f32"
    19  	"github.com/gop9/olt/gio/font/opentype"
    20  	gkey "github.com/gop9/olt/gio/io/key"
    21  	"github.com/gop9/olt/gio/io/pointer"
    22  	"github.com/gop9/olt/gio/io/profile"
    23  	"github.com/gop9/olt/gio/io/system"
    24  	"github.com/gop9/olt/gio/op"
    25  	gioclip "github.com/gop9/olt/gio/op/clip"
    26  	"github.com/gop9/olt/gio/op/paint"
    27  	"github.com/gop9/olt/gio/text"
    28  	"github.com/gop9/olt/gio/unit"
    29  
    30  	"github.com/gop9/olt/framework/clipboard"
    31  	"github.com/gop9/olt/framework/command"
    32  	"github.com/gop9/olt/framework/font"
    33  	"github.com/gop9/olt/framework/label"
    34  	"github.com/gop9/olt/framework/rect"
    35  
    36  	ifont "golang.org/x/image/font"
    37  	"golang.org/x/image/math/fixed"
    38  )
    39  
    40  type masterWindow struct {
    41  	masterWindowCommon
    42  
    43  	Title       string
    44  	initialSize image.Point
    45  	size        image.Point
    46  
    47  	w   *app.Window
    48  	ops op.Ops
    49  
    50  	textbuffer bytes.Buffer
    51  
    52  	closed bool
    53  }
    54  
    55  var clipboardStarted bool = false
    56  var clipboardMu sync.Mutex
    57  
    58  func NewMasterWindowSize(flags WindowFlags, title string, sz image.Point, updatefn UpdateFn) MasterWindow {
    59  	ctx := &context{}
    60  	wnd := &masterWindow{}
    61  
    62  	wnd.masterWindowCommonInit(ctx, flags, updatefn, wnd)
    63  
    64  	wnd.Title = title
    65  	wnd.initialSize = sz
    66  
    67  	clipboardMu.Lock()
    68  	if !clipboardStarted {
    69  		clipboardStarted = true
    70  		clipboard.Start()
    71  	}
    72  	clipboardMu.Unlock()
    73  
    74  	return wnd
    75  }
    76  
    77  func (mw *masterWindow) Main() {
    78  	go func() {
    79  		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)))))
    80  		mw.main()
    81  	}()
    82  	go mw.updater()
    83  	app.Main()
    84  }
    85  
    86  func (mw *masterWindow) Lock() {
    87  	mw.uilock.Lock()
    88  }
    89  
    90  func (mw *masterWindow) Unlock() {
    91  	mw.uilock.Unlock()
    92  }
    93  
    94  func (mw *masterWindow) Close() {
    95  	os.Exit(0) // Bad...
    96  }
    97  
    98  func (mw *masterWindow) Closed() bool {
    99  	mw.uilock.Lock()
   100  	defer mw.uilock.Unlock()
   101  	return mw.closed
   102  }
   103  
   104  func (mw *masterWindow) main() {
   105  	for {
   106  		e := <-mw.w.Events()
   107  		switch e := e.(type) {
   108  		case system.DestroyEvent:
   109  			mw.uilock.Lock()
   110  			mw.closed = true
   111  			mw.uilock.Unlock()
   112  			if e.Err != nil {
   113  				fmt.Fprintf(os.Stderr, "error: %v\n", e.Err)
   114  			}
   115  			return
   116  
   117  		case system.FrameEvent:
   118  			mw.size = e.Size
   119  			mw.uilock.Lock()
   120  			mw.prevCmds = mw.prevCmds[:0]
   121  			mw.updateLocked()
   122  			mw.uilock.Unlock()
   123  
   124  			e.Frame(&mw.ops)
   125  		}
   126  	}
   127  }
   128  
   129  func (mw *masterWindow) processPointerEvent(e pointer.Event) {
   130  	switch e.Type {
   131  	case pointer.Release, pointer.Cancel:
   132  		for i := range mw.ctx.Input.Mouse.Buttons {
   133  			btn := &mw.ctx.Input.Mouse.Buttons[i]
   134  			if btn.Down {
   135  				btn.Down = false
   136  				btn.Clicked = true
   137  			}
   138  		}
   139  
   140  	case pointer.Press:
   141  		var button pointer.Buttons
   142  
   143  		switch {
   144  		case e.Buttons.Contain(pointer.ButtonLeft):
   145  			button = pointer.ButtonLeft
   146  		case e.Buttons.Contain(pointer.ButtonRight):
   147  			button = pointer.ButtonRight
   148  		case e.Buttons.Contain(pointer.ButtonMiddle):
   149  			button = pointer.ButtonMiddle
   150  		}
   151  
   152  		if button == pointer.ButtonRight && e.Modifiers.Contain(gkey.ModCtrl) {
   153  			button = pointer.ButtonLeft
   154  		}
   155  
   156  		down := e.Type == pointer.Press
   157  		btn := &mw.ctx.Input.Mouse.Buttons[button]
   158  		if btn.Down == down {
   159  			break
   160  		}
   161  
   162  		if down {
   163  			btn.ClickedPos.X = int(e.Position.X)
   164  			btn.ClickedPos.Y = int(e.Position.Y)
   165  		}
   166  		btn.Clicked = true
   167  		btn.Down = down
   168  
   169  	case pointer.Move:
   170  		mw.ctx.Input.Mouse.Pos.X = int(e.Position.X)
   171  		mw.ctx.Input.Mouse.Pos.Y = int(e.Position.Y)
   172  		mw.ctx.Input.Mouse.Delta = mw.ctx.Input.Mouse.Pos.Sub(mw.ctx.Input.Mouse.Prev)
   173  
   174  		if e.Scroll.Y < 0 {
   175  			mw.ctx.Input.Mouse.ScrollDelta++
   176  		} else if e.Scroll.Y > 0 {
   177  			mw.ctx.Input.Mouse.ScrollDelta--
   178  		}
   179  	}
   180  }
   181  
   182  var runeToCode = map[string]gkey.Code{}
   183  
   184  func init() {
   185  	for i := byte('a'); i <= 'z'; i++ {
   186  		c := gkey.Code((i - 'a') + 4)
   187  		runeToCode[string([]byte{i})] = c
   188  		runeToCode[string([]byte{i - 0x20})] = c
   189  	}
   190  	//
   191  	//runeToCode["\t"] = gkey.CodeTab
   192  	//runeToCode[" "] = gkey.CodeSpacebar
   193  	//runeToCode["-"] = gkey.CodeHyphenMinus
   194  	//runeToCode["="] = gkey.CodeEqualSign
   195  	//runeToCode["["] = gkey.CodeLeftSquareBracket
   196  	//runeToCode["]"] = gkey.CodeRightSquareBracket
   197  	//runeToCode["\\"] = gkey.CodeBackslash
   198  	//runeToCode[";"] = gkey.CodeSemicolon
   199  	//runeToCode["\""] = gkey.CodeApostrophe
   200  	//runeToCode["`"] = gkey.CodeGraveAccent
   201  	//runeToCode[","] = gkey.CodeComma
   202  	//runeToCode["."] = gkey.CodeFullStop
   203  	//runeToCode["/"] = gkey.CodeSlash
   204  	//
   205  	//runeToCode[gkey.NameLeftArrow] = gkey.CodeLeftArrow
   206  	//runeToCode[gkey.NameRightArrow] = gkey.CodeRightArrow
   207  	//runeToCode[gkey.NameUpArrow] = gkey.CodeUpArrow
   208  	//runeToCode[gkey.NameDownArrow] = gkey.CodeDownArrow
   209  	//runeToCode[gkey.NameReturn] = gkey.CodeReturnEnter
   210  	//runeToCode[gkey.NameEnter] = gkey.CodeReturnEnter
   211  	//runeToCode[gkey.NameEscape] = gkey.CodeEscape
   212  	//runeToCode[gkey.NameHome] = gkey.CodeHome
   213  	//runeToCode[gkey.NameEnd] = gkey.CodeEnd
   214  	//runeToCode[gkey.NameDeleteBackward] = gkey.CodeDeleteBackspace
   215  	//runeToCode[gkey.NameDeleteForward] = gkey.CodeDeleteForward
   216  	//runeToCode[gkey.NamePageUp] = gkey.CodePageUp
   217  	//runeToCode[gkey.NamePageDown] = gkey.CodePageDown
   218  }
   219  
   220  func gio2mobileKey(e gkey.Event) gkey.Event {
   221  	var mod gkey.Modifiers
   222  
   223  	//if e.Modifiers.Contain(gkey.ModCommand) {
   224  	//	mod |= gkey.ModMeta
   225  	//}
   226  	//if e.Modifiers.Contain(gkey.ModCtrl) {
   227  	//	mod |= gkey.ModControl
   228  	//}
   229  	//if e.Modifiers.Contain(gkey.ModAlt) {
   230  	//	mod |= gkey.ModAlt
   231  	//}
   232  	//if e.Modifiers.Contain(gkey.ModSuper) {
   233  	//	mod |= gkey.ModMeta
   234  	//}
   235  
   236  	var name rune
   237  
   238  	for _, ch := range e.Name {
   239  		name = ch
   240  		break
   241  	}
   242  
   243  	return gkey.Event{
   244  		Rune:      name,
   245  		Code:      runeToCode[e.Name],
   246  		Modifiers: mod,
   247  		Direction: gkey.DirRelease,
   248  	}
   249  }
   250  
   251  func (w *masterWindow) updater() {
   252  	var down bool
   253  	for {
   254  		if down {
   255  			time.Sleep(10 * time.Millisecond)
   256  		} else {
   257  			time.Sleep(20 * time.Millisecond)
   258  		}
   259  		func() {
   260  			w.uilock.Lock()
   261  			defer w.uilock.Unlock()
   262  			if w.closed {
   263  				return
   264  			}
   265  			changed := atomic.LoadInt32(&w.ctx.changed)
   266  			if changed > 0 {
   267  				atomic.AddInt32(&w.ctx.changed, -1)
   268  				w.w.Invalidate()
   269  			}
   270  		}()
   271  	}
   272  }
   273  
   274  func (mw *masterWindow) updateLocked() {
   275  	perfString := ""
   276  	q := mw.w.Queue()
   277  	for _, e := range q.Events(mw.ctx) {
   278  		switch e := e.(type) {
   279  		case profile.Event:
   280  			perfString = e.Timings
   281  		case pointer.Event:
   282  			mw.processPointerEvent(e)
   283  		case gkey.EditEvent:
   284  			io.WriteString(&mw.textbuffer, e.Text)
   285  
   286  		case gkey.Event:
   287  			switch e.Name {
   288  			case gkey.NameEnter, gkey.NameReturn:
   289  				io.WriteString(&mw.textbuffer, "\n")
   290  			}
   291  			mw.ctx.Input.Keyboard.Keys = append(mw.ctx.Input.Keyboard.Keys, gio2mobileKey(e))
   292  		}
   293  	}
   294  
   295  	mw.ctx.Windows[0].Bounds = rect.Rect{X: 0, Y: 0, W: mw.size.X, H: mw.size.Y}
   296  	in := &mw.ctx.Input
   297  	in.Mouse.clip = nk_null_rect
   298  	in.Keyboard.Text = mw.textbuffer.String()
   299  	mw.textbuffer.Reset()
   300  
   301  	var t0, t1, te time.Time
   302  	if perfUpdate || mw.Perf {
   303  		t0 = time.Now()
   304  	}
   305  
   306  	if dumpFrame && !perfUpdate {
   307  		panic("dumpFrame")
   308  	}
   309  
   310  	mw.ctx.Update()
   311  
   312  	if perfUpdate || mw.Perf {
   313  		t1 = time.Now()
   314  	}
   315  	nprimitives := mw.draw()
   316  	if perfUpdate && nprimitives > 0 {
   317  		te = time.Now()
   318  
   319  		fps := 1.0 / te.Sub(t0).Seconds()
   320  
   321  		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)
   322  	}
   323  	if mw.Perf && nprimitives > 0 {
   324  		te = time.Now()
   325  		fps := 1.0 / te.Sub(t0).Seconds()
   326  
   327  		s := fmt.Sprintf("%0.4fms + %0.4fms (%0.2f)\n%s", t1.Sub(t0).Seconds()*1000, te.Sub(t1).Seconds()*1000, fps, perfString)
   328  
   329  		font := mw.Style().Font
   330  		txt := fontFace2fontFace(&font).layout(s, -1)
   331  
   332  		bounds := image.Point{X: maxLinesWidth(txt.Lines), Y: (txt.Lines[0].Ascent + txt.Lines[0].Descent).Ceil() * 2}
   333  
   334  		pos := mw.size
   335  		pos.Y -= bounds.Y
   336  		pos.X -= bounds.X
   337  
   338  		paintRect := f32.Rectangle{f32.Point{float32(pos.X), float32(pos.Y)}, f32.Point{float32(pos.X + bounds.X), float32(pos.Y + bounds.Y)}}
   339  
   340  		var stack op.StackOp
   341  		stack.Push(&mw.ops)
   342  		paint.ColorOp{Color: color.RGBA{0xff, 0xff, 0xff, 0xff}}.Add(&mw.ops)
   343  		paint.PaintOp{Rect: paintRect}.Add(&mw.ops)
   344  		stack.Pop()
   345  
   346  		drawText(&mw.ops, txt, font, color.RGBA{0x00, 0x00, 0x00, 0xff}, pos, bounds, paintRect)
   347  	}
   348  }
   349  
   350  func (w *masterWindow) draw() int {
   351  	if !w.drawChanged() {
   352  		return 0
   353  	}
   354  
   355  	w.prevCmds = append(w.prevCmds[:0], w.ctx.cmds...)
   356  
   357  	return w.ctx.Draw(&w.ops, w.size, w.Perf)
   358  }
   359  
   360  func (ctx *context) Draw(ops *op.Ops, size image.Point, perf bool) int {
   361  	ops.Reset()
   362  
   363  	if perf {
   364  		profile.Op{ctx}.Add(ops)
   365  	}
   366  	pointer.InputOp{ctx, false}.Add(ops)
   367  	gkey.InputOp{ctx, true}.Add(ops)
   368  
   369  	var scissorStack op.StackOp
   370  	scissorless := true
   371  
   372  	for i := range ctx.cmds {
   373  		icmd := &ctx.cmds[i]
   374  		switch icmd.Kind {
   375  		case command.ScissorCmd:
   376  			if !scissorless {
   377  				scissorStack.Pop()
   378  			}
   379  			scissorStack.Push(ops)
   380  			gioclip.Rect{Rect: n2fRect(icmd.Rect)}.Op(ops).Add(ops)
   381  			scissorless = false
   382  
   383  		case command.LineCmd:
   384  			cmd := icmd.Line
   385  
   386  			var stack op.StackOp
   387  			stack.Push(ops)
   388  			paint.ColorOp{Color: cmd.Color}.Add(ops)
   389  
   390  			h1 := int(cmd.LineThickness / 2)
   391  			h2 := int(cmd.LineThickness) - h1
   392  
   393  			if cmd.Begin.X == cmd.End.X {
   394  				y0, y1 := cmd.Begin.Y, cmd.End.Y
   395  				if y0 > y1 {
   396  					y0, y1 = y1, y0
   397  				}
   398  				paint.PaintOp{Rect: f32.Rectangle{
   399  					f32.Point{float32(cmd.Begin.X - h1), float32(y0)},
   400  					f32.Point{float32(cmd.Begin.X + h2), float32(y1)}}}.Add(ops)
   401  			} else if cmd.Begin.Y == cmd.End.Y {
   402  				x0, x1 := cmd.Begin.X, cmd.End.X
   403  				if x0 > x1 {
   404  					x0, x1 = x1, x0
   405  				}
   406  				paint.PaintOp{Rect: f32.Rectangle{
   407  					f32.Point{float32(x0), float32(cmd.Begin.Y - h1)},
   408  					f32.Point{float32(x1), float32(cmd.Begin.Y + h2)}}}.Add(ops)
   409  			} else {
   410  				m := float32(cmd.Begin.Y-cmd.End.Y) / float32(cmd.Begin.X-cmd.End.X)
   411  				invm := -1 / m
   412  
   413  				xadv := float32(math.Sqrt(float64(cmd.LineThickness*cmd.LineThickness) / (4 * float64((invm*invm + 1)))))
   414  				yadv := xadv * invm
   415  
   416  				var p gioclip.Path
   417  				p.Begin(ops)
   418  
   419  				pa := f32.Point{float32(cmd.Begin.X) - xadv, float32(cmd.Begin.Y) - yadv}
   420  				p.Move(pa)
   421  				pb := f32.Point{2 * xadv, 2 * yadv}
   422  				p.Line(pb)
   423  				pc := f32.Point{float32(cmd.End.X - cmd.Begin.X), float32(cmd.End.Y - cmd.Begin.Y)}
   424  				p.Line(pc)
   425  				pd := f32.Point{-2 * xadv, -2 * yadv}
   426  				p.Line(pd)
   427  				p.Line(f32.Point{float32(cmd.Begin.X - cmd.End.X), float32(cmd.Begin.Y - cmd.End.Y)})
   428  
   429  				p.End().Add(ops)
   430  
   431  				pb = pb.Add(pa)
   432  				pc = pc.Add(pb)
   433  				pd = pd.Add(pc)
   434  
   435  				minp := f32.Point{
   436  					min4(pa.X, pb.X, pc.X, pd.X),
   437  					min4(pa.Y, pb.Y, pc.Y, pd.Y)}
   438  				maxp := f32.Point{
   439  					max4(pa.X, pb.X, pc.X, pd.X),
   440  					max4(pa.Y, pb.Y, pc.Y, pd.Y)}
   441  
   442  				paint.PaintOp{Rect: f32.Rectangle{minp, maxp}}.Add(ops)
   443  			}
   444  
   445  			stack.Pop()
   446  
   447  		case command.RectFilledCmd:
   448  			cmd := icmd.RectFilled
   449  			// rounding is true if rounding has been requested AND we can draw it
   450  			rounding := cmd.Rounding > 0 && int(cmd.Rounding*2) < icmd.W && int(cmd.Rounding*2) < icmd.H
   451  
   452  			var stack op.StackOp
   453  			stack.Push(ops)
   454  			paint.ColorOp{Color: cmd.Color}.Add(ops)
   455  
   456  			if rounding {
   457  				const c = 0.55228475 // 4*(sqrt(2)-1)/3
   458  
   459  				x, y, w, h := float32(icmd.X), float32(icmd.Y), float32(icmd.W), float32(icmd.H)
   460  				r := float32(cmd.Rounding)
   461  
   462  				var b gioclip.Path
   463  				b.Begin(ops)
   464  				b.Move(f32.Point{X: x + w, Y: y + h - r})
   465  				b.Cube(f32.Point{X: 0, Y: r * c}, f32.Point{X: -r + r*c, Y: r}, f32.Point{X: -r, Y: r}) // SE
   466  				b.Line(f32.Point{X: r - w + r, Y: 0})
   467  				b.Cube(f32.Point{X: -r * c, Y: 0}, f32.Point{X: -r, Y: -r + r*c}, f32.Point{X: -r, Y: -r}) // SW
   468  				b.Line(f32.Point{X: 0, Y: r - h + r})
   469  				b.Cube(f32.Point{X: 0, Y: -r * c}, f32.Point{X: r - r*c, Y: -r}, f32.Point{X: r, Y: -r}) // NW
   470  				b.Line(f32.Point{X: w - r - r, Y: 0})
   471  				b.Cube(f32.Point{X: r * c, Y: 0}, f32.Point{X: r, Y: r - r*c}, f32.Point{X: r, Y: r}) // NE
   472  				b.End().Add(ops)
   473  			}
   474  
   475  			paint.PaintOp{Rect: n2fRect(icmd.Rect)}.Add(ops)
   476  			stack.Pop()
   477  
   478  		case command.TriangleFilledCmd:
   479  			cmd := icmd.TriangleFilled
   480  
   481  			var stack op.StackOp
   482  			stack.Push(ops)
   483  
   484  			paint.ColorOp{cmd.Color}.Add(ops)
   485  
   486  			var p gioclip.Path
   487  			p.Begin(ops)
   488  			p.Move(f32.Point{float32(cmd.A.X), float32(cmd.A.Y)})
   489  			p.Line(f32.Point{float32(cmd.B.X - cmd.A.X), float32(cmd.B.Y - cmd.A.Y)})
   490  			p.Line(f32.Point{float32(cmd.C.X - cmd.B.X), float32(cmd.C.Y - cmd.B.Y)})
   491  			p.Line(f32.Point{float32(cmd.A.X - cmd.C.X), float32(cmd.A.Y - cmd.C.Y)})
   492  			p.End().Add(ops)
   493  
   494  			pmin := f32.Point{
   495  				min2(min2(float32(cmd.A.X), float32(cmd.B.X)), float32(cmd.C.X)),
   496  				min2(min2(float32(cmd.A.Y), float32(cmd.B.Y)), float32(cmd.C.Y))}
   497  
   498  			pmax := f32.Point{
   499  				max2(max2(float32(cmd.A.X), float32(cmd.B.X)), float32(cmd.C.X)),
   500  				max2(max2(float32(cmd.A.Y), float32(cmd.B.Y)), float32(cmd.C.Y))}
   501  
   502  			paint.PaintOp{Rect: f32.Rectangle{pmin, pmax}}.Add(ops)
   503  
   504  			stack.Pop()
   505  
   506  		case command.CircleFilledCmd:
   507  			var stack op.StackOp
   508  			stack.Push(ops)
   509  
   510  			paint.ColorOp{icmd.CircleFilled.Color}.Add(ops)
   511  
   512  			r := min2(float32(icmd.W), float32(icmd.H)) / 2
   513  
   514  			const c = 0.55228475 // 4*(sqrt(2)-1)/3
   515  			var b gioclip.Path
   516  			b.Begin(ops)
   517  			b.Move(f32.Point{X: float32(icmd.X) + r*2, Y: float32(icmd.Y) + r})
   518  			b.Cube(f32.Point{X: 0, Y: r * c}, f32.Point{X: -r + r*c, Y: r}, f32.Point{X: -r, Y: r})    // SE
   519  			b.Cube(f32.Point{X: -r * c, Y: 0}, f32.Point{X: -r, Y: -r + r*c}, f32.Point{X: -r, Y: -r}) // SW
   520  			b.Cube(f32.Point{X: 0, Y: -r * c}, f32.Point{X: r - r*c, Y: -r}, f32.Point{X: r, Y: -r})   // NW
   521  			b.Cube(f32.Point{X: r * c, Y: 0}, f32.Point{X: r, Y: r - r*c}, f32.Point{X: r, Y: r})      // NE
   522  			b.End().Add(ops)
   523  
   524  			paint.PaintOp{Rect: n2fRect(icmd.Rect)}.Add(ops)
   525  
   526  			stack.Pop()
   527  
   528  		case command.ImageCmd:
   529  			var stack op.StackOp
   530  			stack.Push(ops)
   531  			//TODO: this should be retained between frames somehow...
   532  			paint.NewImageOp(icmd.Image.Img).Add(ops)
   533  			paint.PaintOp{n2fRect(icmd.Rect)}.Add(ops)
   534  			stack.Pop()
   535  
   536  		case command.TextCmd:
   537  			txt := fontFace2fontFace(&icmd.Text.Face).layout(icmd.Text.String, -1)
   538  			if len(txt.Lines) <= 0 {
   539  				continue
   540  			}
   541  
   542  			bounds := image.Point{X: maxLinesWidth(txt.Lines), Y: (txt.Lines[0].Ascent + txt.Lines[0].Descent).Ceil()}
   543  			if bounds.X > icmd.W {
   544  				bounds.X = icmd.W
   545  			}
   546  			if bounds.Y > icmd.H {
   547  				bounds.Y = icmd.H
   548  			}
   549  
   550  			drawText(ops, txt, icmd.Text.Face, icmd.Text.Foreground, image.Point{icmd.X, icmd.Y}, bounds, n2fRect(icmd.Rect))
   551  
   552  		default:
   553  			panic(UnknownCommandErr)
   554  		}
   555  	}
   556  
   557  	return len(ctx.cmds)
   558  }
   559  
   560  func n2fRect(r rect.Rect) f32.Rectangle {
   561  	return f32.Rectangle{
   562  		Min: f32.Point{float32(r.X), float32(r.Y)},
   563  		Max: f32.Point{float32(r.X + r.W), float32(r.Y + r.H)}}
   564  }
   565  
   566  func i2fRect(r image.Rectangle) f32.Rectangle {
   567  	return f32.Rectangle{
   568  		Min: f32.Point{X: float32(r.Min.X), Y: float32(r.Min.Y)},
   569  		Max: f32.Point{X: float32(r.Max.X), Y: float32(r.Max.Y)}}
   570  }
   571  
   572  func min4(a, b, c, d float32) float32 {
   573  	return min2(min2(a, b), min2(c, d))
   574  }
   575  
   576  func min2(a, b float32) float32 {
   577  	if a < b {
   578  		return a
   579  	}
   580  	return b
   581  }
   582  
   583  func max4(a, b, c, d float32) float32 {
   584  	return max2(max2(a, b), max2(c, d))
   585  }
   586  
   587  func max2(a, b float32) float32 {
   588  	if a > b {
   589  		return a
   590  	}
   591  	return b
   592  }
   593  
   594  func textPadding(lines []text.Line) (padding image.Rectangle) {
   595  	if len(lines) == 0 {
   596  		return
   597  	}
   598  	first := lines[0]
   599  	if d := first.Ascent + first.Bounds.Min.Y; d < 0 {
   600  		padding.Min.Y = d.Ceil()
   601  	}
   602  	last := lines[len(lines)-1]
   603  	if d := last.Bounds.Max.Y - last.Descent; d > 0 {
   604  		padding.Max.Y = d.Ceil()
   605  	}
   606  	if d := first.Bounds.Min.X; d < 0 {
   607  		padding.Min.X = d.Ceil()
   608  	}
   609  	if d := first.Bounds.Max.X - first.Width; d > 0 {
   610  		padding.Max.X = d.Ceil()
   611  	}
   612  	return
   613  }
   614  
   615  func clipLine(line text.Line, clipObject image.Rectangle) (text.String, f32.Point, bool) {
   616  	off := fixed.Point26_6{X: fixed.I(0), Y: fixed.I(line.Ascent.Ceil())}
   617  	str := line.Text
   618  	for len(str.Advances) > 0 {
   619  		adv := str.Advances[0]
   620  		if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= clipObject.Min.X {
   621  			break
   622  		}
   623  		off.X += adv
   624  		_, s := utf8.DecodeRuneInString(str.String)
   625  		str.String = str.String[s:]
   626  		str.Advances = str.Advances[1:]
   627  	}
   628  	n := 0
   629  	endx := off.X
   630  	for i, adv := range str.Advances {
   631  		if (endx + line.Bounds.Min.X).Floor() > clipObject.Max.X {
   632  			str.String = str.String[:n]
   633  			str.Advances = str.Advances[:i]
   634  			break
   635  		}
   636  		_, s := utf8.DecodeRuneInString(str.String[n:])
   637  		n += s
   638  		endx += adv
   639  	}
   640  	offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64}
   641  	return str, offf, true
   642  }
   643  
   644  func maxLinesWidth(lines []text.Line) int {
   645  	w := 0
   646  	for _, line := range lines {
   647  		if line.Width.Ceil() > w {
   648  			w = line.Width.Ceil()
   649  		}
   650  	}
   651  	return w
   652  }
   653  
   654  func drawText(ops *op.Ops, txt *text.Layout, face font.Face, fgcolor color.RGBA, pos, bounds image.Point, paintRect f32.Rectangle) {
   655  	clipObject := textPadding(txt.Lines)
   656  	clipObject.Max = clipObject.Max.Add(bounds)
   657  
   658  	var stack op.StackOp
   659  	stack.Push(ops)
   660  	paint.ColorOp{fgcolor}.Add(ops)
   661  
   662  	fc := fontFace2fontFace(&face)
   663  
   664  	for i := range txt.Lines {
   665  		txtstr, off, ok := clipLine(txt.Lines[i], clipObject)
   666  		if !ok {
   667  			continue
   668  		}
   669  
   670  		off.X += float32(pos.X)
   671  		off.Y += float32(pos.Y) + float32(i*FontHeight(face))
   672  
   673  		var stack op.StackOp
   674  		stack.Push(ops)
   675  
   676  		op.TransformOp{}.Offset(off).Add(ops)
   677  		fc.shape(txtstr).Add(ops)
   678  
   679  		paint.PaintOp{Rect: paintRect.Sub(off)}.Add(ops)
   680  
   681  		stack.Pop()
   682  	}
   683  
   684  	stack.Pop()
   685  }
   686  
   687  type fontFace struct {
   688  	fnt     *opentype.Font
   689  	shaper  *text.Shaper
   690  	size    int
   691  	fsize   fixed.Int26_6
   692  	metrics ifont.Metrics
   693  }
   694  
   695  func fontFace2fontFace(f *font.Face) *fontFace {
   696  	return (*fontFace)(unsafe.Pointer(f))
   697  }
   698  
   699  func (face *fontFace) layout(str string, width int) *text.Layout {
   700  	if width < 0 {
   701  		width = 1e6
   702  	}
   703  	return face.shaper.Layout(face, text.Font{}, str, text.LayoutOptions{MaxWidth: width})
   704  }
   705  
   706  func (face *fontFace) shape(txtstr text.String) op.CallOp {
   707  	return face.shaper.Shape(face, text.Font{}, txtstr)
   708  }
   709  
   710  func (face *fontFace) Px(v unit.Value) int {
   711  	return face.size
   712  }
   713  
   714  func ChangeFontWidthCache(size int) {
   715  }
   716  
   717  func FontWidth(f font.Face, str string) int {
   718  	txt := fontFace2fontFace(&f).layout(str, -1)
   719  	maxw := 0
   720  	for i := range txt.Lines {
   721  		if w := txt.Lines[i].Width.Ceil(); w > maxw {
   722  			maxw = w
   723  		}
   724  	}
   725  	return maxw
   726  }
   727  
   728  func glyphAdvance(f font.Face, ch rune) int {
   729  	txt := fontFace2fontFace(&f).layout(string(ch), 1e6)
   730  	return txt.Lines[0].Width.Ceil()
   731  }
   732  
   733  func measureRunes(f font.Face, runes []rune) int {
   734  	text := fontFace2fontFace(&f).layout(string(runes), 1e6)
   735  	w := fixed.Int26_6(0)
   736  	for i := range text.Lines {
   737  		w += text.Lines[i].Width
   738  	}
   739  	return w.Ceil()
   740  }
   741  
   742  ///////////////////////////////////////////////////////////////////////////////////
   743  // TEXT WIDGETS
   744  ///////////////////////////////////////////////////////////////////////////////////
   745  
   746  const (
   747  	tabSizeInSpaces = 8
   748  )
   749  
   750  type textWidget struct {
   751  	Padding    image.Point
   752  	Background color.RGBA
   753  	Text       color.RGBA
   754  }
   755  
   756  func widgetText(o *command.Buffer, b rect.Rect, str string, t *textWidget, a label.Align, f font.Face) {
   757  	b.H = max(b.H, 2*t.Padding.Y)
   758  	lblrect := rect.Rect{X: 0, W: 0, Y: b.Y + t.Padding.Y, H: b.H - 2*t.Padding.Y}
   759  
   760  	/* align in x-axis */
   761  	switch a[0] {
   762  	case 'L':
   763  		lblrect.X = b.X + t.Padding.X
   764  		lblrect.W = max(0, b.W-2*t.Padding.X)
   765  	case 'C':
   766  		text_width := FontWidth(f, str)
   767  		text_width += (2.0 * t.Padding.X)
   768  		lblrect.W = max(1, 2*t.Padding.X+text_width)
   769  		lblrect.X = (b.X + t.Padding.X + ((b.W-2*t.Padding.X)-lblrect.W)/2)
   770  		lblrect.X = max(b.X+t.Padding.X, lblrect.X)
   771  		lblrect.W = min(b.X+b.W, lblrect.X+lblrect.W)
   772  		if lblrect.W >= lblrect.X {
   773  			lblrect.W -= lblrect.X
   774  		}
   775  	case 'R':
   776  		text_width := FontWidth(f, str)
   777  		text_width += (2.0 * t.Padding.X)
   778  		lblrect.X = max(b.X+t.Padding.X, (b.X+b.W)-(2*t.Padding.X+text_width))
   779  		lblrect.W = text_width + 2*t.Padding.X
   780  	default:
   781  		panic("unsupported alignment")
   782  	}
   783  
   784  	/* align in y-axis */
   785  	if len(a) >= 2 {
   786  		switch a[1] {
   787  		case 'C':
   788  			lblrect.Y = b.Y + b.H/2.0 - FontHeight(f)/2.0
   789  		case 'B':
   790  			lblrect.Y = b.Y + b.H - FontHeight(f)
   791  		}
   792  	}
   793  	if lblrect.H < FontHeight(f)*2 {
   794  		lblrect.H = FontHeight(f) * 2
   795  	}
   796  
   797  	o.DrawText(lblrect, str, f, t.Text)
   798  }
   799  
   800  func widgetTextWrap(o *command.Buffer, b rect.Rect, str []rune, t *textWidget, f font.Face) {
   801  	var text textWidget
   802  
   803  	text.Padding = image.Point{0, 0}
   804  	text.Background = t.Background
   805  	text.Text = t.Text
   806  
   807  	b.W = max(b.W, 2*t.Padding.X)
   808  	b.H = max(b.H, 2*t.Padding.Y)
   809  	b.H = b.H - 2*t.Padding.Y
   810  
   811  	var line rect.Rect
   812  	line.X = b.X + t.Padding.X
   813  	line.Y = b.Y + t.Padding.Y
   814  	line.W = b.W - 2*t.Padding.X
   815  	line.H = 2*t.Padding.Y + FontHeight(f)
   816  
   817  	lines := fontFace2fontFace(&f).layout(string(str), line.W)
   818  
   819  	for _, txtline := range lines.Lines {
   820  		if line.Y+line.H >= (b.Y + b.H) {
   821  			break
   822  		}
   823  		widgetText(o, line, txtline.Text.String, &text, "LC", f)
   824  		line.Y += FontHeight(f) + 2*t.Padding.Y
   825  	}
   826  }