diff --git a/_examples/demo/demo.go b/_examples/demo/demo.go index c07599f..51b4a77 100644 --- a/_examples/demo/demo.go +++ b/_examples/demo/demo.go @@ -11,14 +11,12 @@ import ( "time" "github.com/aarzilli/nucular" + "github.com/aarzilli/nucular/font" "github.com/aarzilli/nucular/label" "github.com/aarzilli/nucular/rect" nstyle "github.com/aarzilli/nucular/style" "github.com/aarzilli/nucular/style-editor" - "github.com/golang/freetype" - "github.com/golang/freetype/truetype" - "golang.org/x/image/font" "golang.org/x/mobile/event/key" ) @@ -127,11 +125,11 @@ func main() { normalFontData, normerr := ioutil.ReadFile("demofont.ttf") if normerr == nil { - normalTtfont, normerr := freetype.ParseFont(normalFontData) + szf := 12 * scaling + face, normerr := font.NewFace(normalFontData, int(szf)) if normerr == nil { style := Wnd.Style() - szf := 12 * scaling - style.Font = truetype.NewFace(normalTtfont, &truetype.Options{Size: float64(int(szf)), Hinting: font.HintingFull, DPI: 72}) + style.Font = face } } diff --git a/_examples/richtext/main.go b/_examples/richtext/main.go index b39bb1d..f15c1b3 100644 --- a/_examples/richtext/main.go +++ b/_examples/richtext/main.go @@ -10,9 +10,7 @@ import ( "github.com/aarzilli/nucular/style" "github.com/aarzilli/nucular/_examples/richtext/internal/assets" - "github.com/golang/freetype" - "github.com/golang/freetype/truetype" - "golang.org/x/image/font" + "github.com/aarzilli/nucular/font" ) //go:generate go-bindata -o internal/assets/assets.go -pkg assets DejaVuSans.ttf DejaVuSans-Bold.ttf DejaVuSans-Oblique.ttf @@ -34,20 +32,15 @@ func main() { wnd.SetStyle(style.FromTheme(style.DarkTheme, 2.0)) regularData, _ := assets.Asset("DejaVuSans.ttf") - ttfRegular, _ := freetype.ParseFont(regularData) - boldData, _ := assets.Asset("DejaVuSans-Bold.ttf") - ttfBold, _ := freetype.ParseFont(boldData) - italicData, _ := assets.Asset("DejaVuSans-Oblique.ttf") - ttfItalic, _ := freetype.ParseFont(italicData) - proportional = truetype.NewFace(ttfRegular, &truetype.Options{Size: float64(int(float64(12) * wnd.Style().Scaling)), Hinting: font.HintingFull, DPI: 72}) - header = truetype.NewFace(ttfRegular, &truetype.Options{Size: float64(int(float64(21) * wnd.Style().Scaling)), Hinting: font.HintingFull, DPI: 72}) + proportional, _ = font.NewFace(regularData, int(float64(12)*wnd.Style().Scaling)) + header, _ = font.NewFace(regularData, int(float64(21)*wnd.Style().Scaling)) monospace = wnd.Style().Font - bold = truetype.NewFace(ttfBold, &truetype.Options{Size: float64(int(float64(12) * wnd.Style().Scaling)), Hinting: font.HintingFull, DPI: 72}) - italic = truetype.NewFace(ttfItalic, &truetype.Options{Size: float64(int(float64(12) * wnd.Style().Scaling)), Hinting: font.HintingFull, DPI: 72}) + bold, _ = font.NewFace(boldData, int(float64(12)*wnd.Style().Scaling)) + italic, _ = font.NewFace(italicData, int(float64(12)*wnd.Style().Scaling)) searchEd.Flags = nucular.EditField diff --git a/command/command.go b/command/command.go index 1068f35..5e1de0b 100644 --- a/command/command.go +++ b/command/command.go @@ -4,9 +4,8 @@ import ( "image" "image/color" + "github.com/aarzilli/nucular/font" "github.com/aarzilli/nucular/rect" - - "golang.org/x/image/font" ) // CommandBuffer is a list of drawing directives. @@ -69,10 +68,6 @@ type CircleFilled struct { Color color.RGBA } -type Image struct { - Img *image.RGBA -} - type Text struct { Face font.Face Foreground color.RGBA @@ -152,18 +147,6 @@ func (b *Buffer) FillTriangle(p0, p1, p2 image.Point, c color.RGBA) { b.Commands = append(b.Commands, cmd) } -func (b *Buffer) DrawImage(r rect.Rect, img *image.RGBA) { - if !r.Intersect(&b.Clip) { - return - } - - var cmd Command - cmd.Kind = ImageCmd - cmd.Rect = r - cmd.Image.Img = img - b.Commands = append(b.Commands, cmd) -} - func (b *Buffer) DrawText(r rect.Rect, str string, face font.Face, fg color.RGBA) { if len(str) == 0 || (fg.A == 0) { return diff --git a/command/command_gio.go b/command/command_gio.go new file mode 100644 index 0000000..dcb098a --- /dev/null +++ b/command/command_gio.go @@ -0,0 +1,26 @@ +// +build nucular_gio + +package command + +import ( + "image" + + "gioui.org/op/paint" + "github.com/aarzilli/nucular/rect" +) + +type Image struct { + Img paint.ImageOp +} + +func (b *Buffer) DrawImage(r rect.Rect, img *image.RGBA) { + if !r.Intersect(&b.Clip) { + return + } + + var cmd Command + cmd.Kind = ImageCmd + cmd.Rect = r + cmd.Image.Img = paint.NewImageOp(img) + b.Commands = append(b.Commands, cmd) +} diff --git a/command/command_shiny.go b/command/command_shiny.go new file mode 100644 index 0000000..9b273b0 --- /dev/null +++ b/command/command_shiny.go @@ -0,0 +1,24 @@ +// +build !nucular_gio + +package command + +import ( + "github.com/aarzilli/nucular/rect" + "image" +) + +type Image struct { + Img *image.RGBA +} + +func (b *Buffer) DrawImage(r rect.Rect, img *image.RGBA) { + if !r.Intersect(&b.Clip) { + return + } + + var cmd Command + cmd.Kind = ImageCmd + cmd.Rect = r + cmd.Image.Img = img + b.Commands = append(b.Commands, cmd) +} diff --git a/context.go b/context.go index c36d5ba..102a852 100644 --- a/context.go +++ b/context.go @@ -3,22 +3,15 @@ package nucular import ( "bytes" "errors" - "fmt" "image" - "image/color" "image/draw" "io" - "math" "time" "github.com/aarzilli/nucular/command" "github.com/aarzilli/nucular/rect" nstyle "github.com/aarzilli/nucular/style" - "github.com/golang/freetype/raster" - - "golang.org/x/image/font" - "golang.org/x/image/math/fixed" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/mouse" ) @@ -347,435 +340,6 @@ func (ctx *context) restackClick(w *Window) bool { return false } -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 := font.Drawer{ - Dst: dstimg, - Src: image.NewUniform(icmd.Text.Foreground), - Face: icmd.Text.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 - } - } - } -} - type dockedNodeType uint8 const ( diff --git a/drawing.go b/drawing.go index 5c966d8..556c192 100644 --- a/drawing.go +++ b/drawing.go @@ -5,11 +5,10 @@ import ( "image/color" "github.com/aarzilli/nucular/command" + "github.com/aarzilli/nucular/font" "github.com/aarzilli/nucular/label" "github.com/aarzilli/nucular/rect" nstyle "github.com/aarzilli/nucular/style" - - "golang.org/x/image/font" ) func drawSymbol(out *command.Buffer, type_ label.SymbolType, content rect.Rect, background color.RGBA, foreground color.RGBA, border_width int, font font.Face) { diff --git a/font/font.go b/font/font.go new file mode 100644 index 0000000..da35539 --- /dev/null +++ b/font/font.go @@ -0,0 +1,15 @@ +package font + +import ( + "github.com/aarzilli/nucular/internal/assets" +) + +// Returns default font (DroidSansMono) with specified size and scaling +func DefaultFont(size int, scaling float64) Face { + fontData, _ := assets.Asset("DroidSansMono.ttf") + face, err := NewFace(fontData, int(float64(size)*scaling)) + if err != nil { + panic(err) + } + return face +} diff --git a/font/font_gio.go b/font/font_gio.go new file mode 100644 index 0000000..a781dbc --- /dev/null +++ b/font/font_gio.go @@ -0,0 +1,59 @@ +// +build nucular_gio + +package font + +import ( + "crypto/md5" + "sync" + + "gioui.org/font/opentype" + "gioui.org/text" + "gioui.org/unit" + + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" +) + +type Face struct { + fnt *opentype.Font + shaper *text.Shaper + size int + fsize fixed.Int26_6 + metrics font.Metrics +} + +var fontsMu sync.Mutex +var fontsMap = map[[md5.Size]byte]*opentype.Font{} + +func NewFace(ttf []byte, size int) (Face, error) { + key := md5.Sum(ttf) + fontsMu.Lock() + defer fontsMu.Unlock() + + fnt, _ := fontsMap[key] + if fnt == nil { + var err error + fnt, err = opentype.Parse(ttf) + if err != nil { + return Face{}, err + } + } + + shaper := &text.Shaper{} + shaper.Register(text.Font{}, fnt) + + face := Face{fnt, shaper, size, fixed.I(size), font.Metrics{}} + metricsTxt := face.shaper.Layout(face, text.Font{}, "metrics", text.LayoutOptions{MaxWidth: 1e6}) + face.metrics.Ascent = metricsTxt.Lines[0].Ascent + face.metrics.Descent = metricsTxt.Lines[0].Descent + face.metrics.Height = face.metrics.Ascent + face.metrics.Descent + return face, nil +} + +func (face Face) Px(v unit.Value) int { + return face.size +} + +func (face Face) Metrics() font.Metrics { + return face.metrics +} diff --git a/font/font_shiny.go b/font/font_shiny.go new file mode 100644 index 0000000..fb6ffe1 --- /dev/null +++ b/font/font_shiny.go @@ -0,0 +1,42 @@ +//+build !nucular_gio + +package font + +import ( + "crypto/md5" + "sync" + + "golang.org/x/image/font" + + "github.com/golang/freetype" + "github.com/golang/freetype/truetype" +) + +type Face struct { + face font.Face +} + +var fontsMu sync.Mutex +var fontsMap = map[[md5.Size]byte]*truetype.Font{} + +// NewFace returns a new face by parsing the ttf font. +func NewFace(ttf []byte, size int) (Face, error) { + key := md5.Sum(ttf) + fontsMu.Lock() + defer fontsMu.Unlock() + + fnt, _ := fontsMap[key] + if fnt == nil { + var err error + fnt, err = freetype.ParseFont(ttf) + if err != nil { + return Face{}, err + } + } + + return Face{truetype.NewFace(fnt, &truetype.Options{Size: float64(size), Hinting: font.HintingFull, DPI: 72})}, nil +} + +func (face Face) Metrics() font.Metrics { + return face.face.Metrics() +} diff --git a/gio.go b/gio.go new file mode 100644 index 0000000..82b257a --- /dev/null +++ b/gio.go @@ -0,0 +1,662 @@ +// +build nucular_gio + +package nucular + +import ( + "bytes" + "fmt" + "image" + "image/color" + "io" + "math" + "os" + "sync/atomic" + "time" + "unicode/utf8" + + "gioui.org/app" + "gioui.org/f32" + "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/command" + "github.com/aarzilli/nucular/font" + "github.com/aarzilli/nucular/rect" + + "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 +} + +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 + + 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] + 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 + } + + 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 +} + +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: + 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.Lines), Y: (txt.Lines[0].Ascent + txt.Lines[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) + 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.Lines) <= 0 { + continue + } + + bounds := image.Point{X: maxLinesWidth(txt.Lines), Y: (txt.Lines[0].Ascent + txt.Lines[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.String, f32.Point, bool) { + off := fixed.Point26_6{X: fixed.I(0), Y: fixed.I(line.Ascent.Ceil())} + str := line.Text + for len(str.Advances) > 0 { + adv := str.Advances[0] + if (off.X + adv + line.Bounds.Max.X - line.Width).Ceil() >= clip.Min.X { + break + } + off.X += adv + _, s := utf8.DecodeRuneInString(str.String) + str.String = str.String[s:] + str.Advances = str.Advances[1:] + } + n := 0 + endx := off.X + for i, adv := range str.Advances { + if (endx + line.Bounds.Min.X).Floor() > clip.Max.X { + str.String = str.String[:n] + str.Advances = str.Advances[:i] + break + } + _, s := utf8.DecodeRuneInString(str.String[n:]) + n += s + endx += adv + } + offf := f32.Point{X: float32(off.X) / 64, Y: float32(off.Y) / 64} + return str, 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.Layout, face font.Face, fgcolor color.RGBA, pos, bounds image.Point, paintRect f32.Rectangle) { + clip := textPadding(txt.Lines) + 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.Lines { + txtstr, off, ok := clipLine(txt.Lines[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).Add(ops) + + paint.PaintOp{Rect: paintRect.Sub(off)}.Add(ops) + + stack.Pop() + } + + stack.Pop() +} diff --git a/go.mod b/go.mod index be64ffa..3c3d273 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/aarzilli/nucular require ( + gioui.org v0.0.0-20191120192806-6a2b5a8d3b93 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 github.com/golang/freetype v0.0.0-20161208064710-d9be45aaf745 github.com/hashicorp/golang-lru v0.0.0-20160813221303-0a025b7e63ad diff --git a/go.sum b/go.sum index 8904587..4f2450e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,11 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +gioui.org v0.0.0-20191030131124-6bbfe288aae5 h1:ZMC6gD4QMd7vOfrfvsZ7jKeDmauQo2dVRcu4y/AXI9w= +gioui.org v0.0.0-20191030131124-6bbfe288aae5/go.mod h1:KqFFi2Dq5gYA3FJ0sDOt8OBXoMsuxMtE8v2f0JExXAY= +gioui.org v0.0.0-20191030221049-b3d4da62296f h1:k6HE7RkNnmlmSKW3lU3B7fL1yllL0ReyAwF6dwphTcY= +gioui.org v0.0.0-20191030221049-b3d4da62296f/go.mod h1:KqFFi2Dq5gYA3FJ0sDOt8OBXoMsuxMtE8v2f0JExXAY= +gioui.org v0.0.0-20191120192806-6a2b5a8d3b93 h1:WU7p1URyFKsFMx5+jYmxUgAyXiMQSZvbp/9YQiziSsQ= +gioui.org v0.0.0-20191120192806-6a2b5a8d3b93/go.mod h1:KqFFi2Dq5gYA3FJ0sDOt8OBXoMsuxMtE8v2f0JExXAY= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= @@ -14,6 +20,7 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSin golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522 h1:OeRHuibLsmZkFj773W4LcfAGsSxJgfPONhr8cmO+eLA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191024150812-c286b889502e h1:fmGnHW8OPmvjJP1J7hROFG77l4AgxYQWmbEyGsBpddg= golang.org/x/exp v0.0.0-20191024150812-c286b889502e/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0= @@ -35,7 +42,11 @@ golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjW golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/masterwindow.go b/masterwindow.go index 1134ea9..1ecf1f3 100644 --- a/masterwindow.go +++ b/masterwindow.go @@ -14,9 +14,6 @@ import ( "github.com/aarzilli/nucular/command" "github.com/aarzilli/nucular/rect" nstyle "github.com/aarzilli/nucular/style" - - "golang.org/x/image/font" - "golang.org/x/image/math/fixed" ) type MasterWindow interface { @@ -169,24 +166,6 @@ func (w *masterWindowCommon) dumpFrame(wimg *image.RGBA, t0, t1, te time.Time, n frameCnt++ } -func (w *masterWindowCommon) drawPerfCounter(img *image.RGBA, bounds image.Rectangle, t0, t1, te time.Time) { - 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 := font.Drawer{ - Dst: img, - Src: image.White, - Face: w.ctx.Style.Font} - - 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) -} - // compares cmds to the last draw frame, returns true if there is a change func (w *masterWindowCommon) drawChanged() bool { diff --git a/nucular.go b/nucular.go index f427acc..64f0c04 100644 --- a/nucular.go +++ b/nucular.go @@ -9,11 +9,11 @@ import ( "sync/atomic" "github.com/aarzilli/nucular/command" + "github.com/aarzilli/nucular/font" "github.com/aarzilli/nucular/label" "github.com/aarzilli/nucular/rect" nstyle "github.com/aarzilli/nucular/style" - "golang.org/x/image/font" "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/mouse" ) diff --git a/nucular_test.go b/nucular_test.go deleted file mode 100644 index 4ac818a..0000000 --- a/nucular_test.go +++ /dev/null @@ -1,131 +0,0 @@ -package nucular - -import ( - "image" - "testing" - - "github.com/aarzilli/nucular/label" - "github.com/aarzilli/nucular/rect" - - "golang.org/x/mobile/event/key" - "golang.org/x/mobile/event/mouse" -) - -func centerOf(rect rect.Rect) image.Point { - return image.Pt(rect.X+rect.W/2, rect.Y+rect.H/2) -} - -func TestContextualReplace(t *testing.T) { - test1cnt := 0 - test2cnt := 0 - test2clicked := 0 - var test1rect, test2rect, lblrect rect.Rect - - w := NewTestWindow(0, image.Pt(640, 480), func(w *Window) { - w.Row(30).Static(180) - w.Label("Right click me for menu", "LC") - lblrect = w.LastWidgetBounds - if w := w.ContextualOpen(0, image.Point{100, 300}, w.LastWidgetBounds, nil); w != nil { - w.Row(25).Dynamic(1) - if r := w.WidgetBounds(); test1cnt == 0 { - test1rect = r - } else if test1rect != r { - t.Fatalf("test item 1 changed position (%d): %v -> %v", test1cnt, test1rect, r) - } - test1cnt++ - if w.MenuItem(label.TA("Test Item", "CC")) { - w.ContextualOpen(WindowContextualReplace, image.Point{100, 300}, rect.Rect{0, 0, 0, 0}, func(w *Window) { - w.Row(25).Dynamic(1) - if r := w.WidgetBounds(); test2cnt == 0 { - test2rect = r - } else if test2rect != r { - t.Fatalf("test item 2 changed position (%d): %v -> %v\n", test2cnt, test2rect, r) - } - test2cnt++ - if w.MenuItem(label.TA("Second Test Item", "CC")) { - test2clicked++ - } - }) - } - } - }) - - w.Update() - w.Click(mouse.ButtonRight, centerOf(lblrect)) - - if test1cnt == 0 { - t.Fatalf("Test item 1 was not displayed") - } - - w.Click(mouse.ButtonLeft, centerOf(test1rect)) - - if test2cnt == 0 { - t.Fatalf("Test item 2 was not displayed") - } - - if test1rect != test2rect { - t.Fatalf("contextual replace failed: %v %v", test1rect, test2rect) - } - - c := test2cnt - w.Update() - if test2cnt == c { - t.Fatalf("second contextual menu closed immediately: %d", test2cnt) - } -} - -func TestWindowEnabledFlagOnGroup(t *testing.T) { - clicked := 0 - var buttonrect rect.Rect - w := NewTestWindow(0, image.Pt(640, 480), func(w *Window) { - w.Row(0).Dynamic(1) - if w := w.GroupBegin("subwindow", 0); w != nil { - w.Row(20).Static(100) - if w.ButtonText("Test button") { - clicked++ - } - buttonrect = w.LastWidgetBounds - w.GroupEnd() - } - }) - - w.Update() - w.Click(mouse.ButtonLeft, centerOf(buttonrect)) - if clicked != 1 { - t.Fatalf("button wasn't clicked") - } -} - -func TestEditorEndKey(t *testing.T) { - const testString = "this is a test string" - const testString2 = testString + "\n" + testString - var ed TextEditor - ed.Flags = EditSelectable | EditMultiline - ed.Active = true - ed.Buffer = []rune(testString) - w := NewTestWindow(0, image.Pt(640, 480), func(w *Window) { - w.Row(0).Dynamic(1) - ed.Edit(w) - }) - - check := func(tgt int, title string) { - if ed.Cursor != tgt { - t.Fatalf("Cursor position %d, expected %d (%s)", ed.Cursor, tgt, title) - } - } - - w.Update() - w.TypeKey(key.Event{Code: key.CodeEnd}) - check(len(testString), "singleline") - - w.TypeKey(key.Event{Code: key.CodeEnd}) - check(len(testString), "singleline 2") - - ed.Cursor = 0 - ed.Buffer = []rune(testString2) - w.TypeKey(key.Event{Code: key.CodeEnd}) - check(len(testString), "multiline") - - w.TypeKey(key.Event{Code: key.CodeEnd}) - check(len(testString), "multiline 2") -} diff --git a/richtext/draw.go b/richtext/draw.go index 9d30eec..1ec7905 100644 --- a/richtext/draw.go +++ b/richtext/draw.go @@ -7,9 +7,9 @@ import ( "github.com/aarzilli/nucular" "github.com/aarzilli/nucular/command" + "github.com/aarzilli/nucular/font" "github.com/aarzilli/nucular/rect" nstyle "github.com/aarzilli/nucular/style" - "golang.org/x/image/font" "golang.org/x/image/math/fixed" "golang.org/x/mobile/event/mouse" ) @@ -314,60 +314,6 @@ func alignBaseline(h int, asc int, face font.Face) int { return d } -func (rtxt *RichText) calcAdvances() { - if rtxt.adv != nil { - rtxt.adv = rtxt.adv[:0] - } - prevch := rune(-1) - advance := fixed.I(0) - var siter styleIterator - siter.Init(rtxt) - for _, chunk := range rtxt.chunks { - // Note chunk is a copy of the element in the slice so we can modify it with impunity - for chunk.len() > 0 { - var ch rune - var rsz int - if chunk.b != nil { - ch, rsz = utf8.DecodeRune(chunk.b) - chunk.b = chunk.b[rsz:] - } else { - ch, rsz = utf8.DecodeRuneInString(chunk.s) - chunk.s = chunk.s[rsz:] - } - - styleSel := siter.styleSel - - if len(rtxt.adv) > 0 { - kern := styleSel.Face.Kern(prevch, ch) - rtxt.adv[len(rtxt.adv)-1] += kern - advance += kern - } - - switch ch { - case '\t': - tabszf, _ := styleSel.Face.GlyphAdvance(' ') - tabszf *= 8 - tabsz := tabszf.Ceil() - a := fixed.I(int((float64(advance.Ceil()+tabsz)/float64(tabsz))*float64(tabsz)) - advance.Ceil()) - rtxt.adv = append(rtxt.adv, a) - advance += a - case '\n': - rtxt.adv = append(rtxt.adv, 0) - advance = 0 - default: - a, _ := styleSel.Face.GlyphAdvance(ch) - rtxt.adv = append(rtxt.adv, a) - advance += a - } - - prevch = ch - if siter.AdvanceRune(rsz) { - prevch = rune(-1) - } - } - } -} - func (rtxt *RichText) reflow() { if rtxt.lines != nil { rtxt.lines = rtxt.lines[:0] @@ -704,7 +650,7 @@ func (siter *styleIterator) fixDefaults() { if siter.styleSel.BgColor == zero { siter.styleSel.BgColor = color.RGBA{0, 0, 0, 0} } - if siter.styleSel.Face == nil { + if siter.styleSel.Face == (font.Face{}) { siter.styleSel.Face = siter.rtxt.face } } diff --git a/richtext/draw_gio.go b/richtext/draw_gio.go new file mode 100644 index 0000000..9e5928f --- /dev/null +++ b/richtext/draw_gio.go @@ -0,0 +1,71 @@ +// +build nucular_gio + +package richtext + +import ( + "unsafe" + + "github.com/aarzilli/nucular/font" + + ifont "golang.org/x/image/font" + + "golang.org/x/image/math/fixed" + + "gioui.org/font/opentype" + "gioui.org/text" + "gioui.org/unit" +) + +type fontFace struct { + fnt *opentype.Font + shaper *text.Shaper + 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.Layout { + if width < 0 { + width = 1e6 + } + return face.shaper.Layout(face, text.Font{}, str, text.LayoutOptions{MaxWidth: width}) +} + +func (face *fontFace) Px(v unit.Value) int { + return face.size +} + +func (rtxt *RichText) calcAdvances() { + if rtxt.adv != nil { + rtxt.adv = rtxt.adv[:0] + } + pos := int32(0) + var siter styleIterator + siter.Init(rtxt) + for _, chunk := range rtxt.chunks { + // Note chunk is a copy of the element in the slice so we can modify it with impunity + for chunk.len() > 0 { + len := chunk.len() + if siter.styleSel.E < pos+len { + len = siter.styleSel.E - pos + } + + if chunk.b != nil { + panic("not implemented") + } + + txt := fontFace2fontFace(&siter.styleSel.Face).layout(chunk.s[:len], 1e6) + for _, line := range txt.Lines { + rtxt.adv = append(rtxt.adv, line.Text.Advances...) + } + + siter.AdvanceTo(pos + len) + pos += len + chunk.s = chunk.s[len:] + } + } +} diff --git a/richtext/draw_shiny.go b/richtext/draw_shiny.go new file mode 100644 index 0000000..545d440 --- /dev/null +++ b/richtext/draw_shiny.go @@ -0,0 +1,63 @@ +// +build !nucular_gio + +package richtext + +import ( + "unicode/utf8" + + "golang.org/x/image/math/fixed" +) + +func (rtxt *RichText) calcAdvances() { + if rtxt.adv != nil { + rtxt.adv = rtxt.adv[:0] + } + prevch := rune(-1) + advance := fixed.I(0) + var siter styleIterator + siter.Init(rtxt) + for _, chunk := range rtxt.chunks { + // Note chunk is a copy of the element in the slice so we can modify it with impunity + for chunk.len() > 0 { + var ch rune + var rsz int + if chunk.b != nil { + ch, rsz = utf8.DecodeRune(chunk.b) + chunk.b = chunk.b[rsz:] + } else { + ch, rsz = utf8.DecodeRuneInString(chunk.s) + chunk.s = chunk.s[rsz:] + } + + styleSel := siter.styleSel + + if len(rtxt.adv) > 0 { + kern := styleSel.Face.Kern(prevch, ch) + rtxt.adv[len(rtxt.adv)-1] += kern + advance += kern + } + + switch ch { + case '\t': + tabszf, _ := styleSel.Face.GlyphAdvance(' ') + tabszf *= 8 + tabsz := tabszf.Ceil() + a := fixed.I(int((float64(advance.Ceil()+tabsz)/float64(tabsz))*float64(tabsz)) - advance.Ceil()) + rtxt.adv = append(rtxt.adv, a) + advance += a + case '\n': + rtxt.adv = append(rtxt.adv, 0) + advance = 0 + default: + a, _ := styleSel.Face.GlyphAdvance(ch) + rtxt.adv = append(rtxt.adv, a) + advance += a + } + + prevch = ch + if siter.AdvanceRune(rsz) { + prevch = rune(-1) + } + } + } +} diff --git a/richtext/rtxt.go b/richtext/rtxt.go index 2da0aa8..075793d 100644 --- a/richtext/rtxt.go +++ b/richtext/rtxt.go @@ -12,7 +12,7 @@ import ( "unicode/utf8" "github.com/aarzilli/nucular" - "golang.org/x/image/font" + "github.com/aarzilli/nucular/font" "golang.org/x/image/math/fixed" ) diff --git a/shiny.go b/shiny.go index 14a2446..87cf7aa 100644 --- a/shiny.go +++ b/shiny.go @@ -1,4 +1,4 @@ -// +build linux,!android,!nucular_mobile darwin,!nucular_mobile windows,!nucular_mobile freebsd,!nucular_mobile +// +build !nucular_gio package nucular @@ -6,12 +6,16 @@ import ( "bytes" "fmt" "image" + "image/color" + "image/draw" + "math" "os" "sync" "sync/atomic" "time" "github.com/aarzilli/nucular/clipboard" + "github.com/aarzilli/nucular/command" "github.com/aarzilli/nucular/rect" "golang.org/x/exp/shiny/driver" @@ -21,6 +25,11 @@ import ( "golang.org/x/mobile/event/mouse" "golang.org/x/mobile/event/paint" "golang.org/x/mobile/event/size" + + "github.com/golang/freetype/raster" + + "golang.org/x/image/font" + "golang.org/x/image/math/fixed" ) //go:generate go-bindata -o internal/assets/assets.go -pkg assets DroidSansMono.ttf @@ -270,7 +279,23 @@ func (w *masterWindow) updateLocked() { } if w.Perf && nprimitives > 0 { te = time.Now() - w.drawPerfCounter(w.wndb.RGBA(), w.bounds, t0, t1, te) + 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 := font.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) @@ -321,3 +346,432 @@ func (w *masterWindow) draw() int { 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 := font.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 + } + } + } +} diff --git a/style/style.go b/style/style.go index 9851f3a..1e168fc 100644 --- a/style/style.go +++ b/style/style.go @@ -3,16 +3,10 @@ package style import ( "image" "image/color" - "sync" "github.com/aarzilli/nucular/command" - "github.com/aarzilli/nucular/internal/assets" + "github.com/aarzilli/nucular/font" "github.com/aarzilli/nucular/label" - - "golang.org/x/image/font" - - "github.com/golang/freetype" - "github.com/golang/freetype/truetype" ) type WidgetStates int @@ -1065,11 +1059,11 @@ func (style *Style) Scale(scaling float64) { style.Scaling = scaling - if style.Font == nil || style.defaultFont == style.Font { + if style.Font == (font.Face{}) || style.defaultFont == style.Font { style.DefaultFont(scaling) style.defaultFont = style.Font } else { - style.defaultFont = nil + style.defaultFont = font.Face{} } style.unscaled.Font = style.Font @@ -1202,30 +1196,15 @@ func (style *Style) Scale(scaling float64) { } func (style *Style) DefaultFont(scaling float64) { - style.Font = defaultFont(12, scaling) + style.Font = font.DefaultFont(12, scaling) style.defaultFont = style.Font } -var ttfontDefault *truetype.Font -var defaultFontInit sync.Once - -// Returns default font (DroidSansMono) with specified size and scaling -func defaultFont(size int, scaling float64) font.Face { - defaultFontInit.Do(func() { - fontData, _ := assets.Asset("DroidSansMono.ttf") - ttfontDefault, _ = freetype.ParseFont(fontData) - }) - - sz := int(float64(size) * scaling) - - return truetype.NewFace(ttfontDefault, &truetype.Options{Size: float64(sz), Hinting: font.HintingFull, DPI: 72}) -} - func (style *Style) Defaults() { if style.Scaling == 0.0 { style.Scaling = 1.0 } - if style.Font == nil { + if style.Font == (font.Face{}) { style.DefaultFont(style.Scaling) } } diff --git a/testing.go b/testing.go deleted file mode 100644 index ba8a0b5..0000000 --- a/testing.go +++ /dev/null @@ -1,143 +0,0 @@ -package nucular - -import ( - "bytes" - "image" - - "github.com/aarzilli/nucular/rect" - nstyle "github.com/aarzilli/nucular/style" - - "golang.org/x/mobile/event/key" - "golang.org/x/mobile/event/mouse" -) - -type TestWindow struct { - Img *image.RGBA - - ctx *context - layout panel -} - -func NewTestWindow(flags WindowFlags, size image.Point, updatefn UpdateFn) *TestWindow { - ctx := &context{} - ctx.Input.Mouse.valid = true - wnd := &TestWindow{ctx: ctx} - wnd.Img = image.NewRGBA(image.Rect(0, 0, size.X, size.Y)) - wnd.layout.Flags = flags - ctx.setupMasterWindow(&wnd.layout, updatefn) - ctx.Windows[0].Bounds = rect.Rect{0, 0, size.X, size.Y} - ctx.mw = wnd - wnd.SetStyle(nstyle.FromTheme(nstyle.DefaultTheme, 1.0)) - return wnd -} - -func (w *TestWindow) context() *context { - return w.ctx -} - -func (w *TestWindow) ActivateEditor(ed *TextEditor) { - w.ctx.activateEditor = ed -} - -func (w *TestWindow) Close() { -} - -func (w *TestWindow) Closed() bool { - return false -} - -func (w *TestWindow) Changed() { -} - -func (w *TestWindow) Main() { -} - -func (w *TestWindow) Lock() { -} - -func (w *TestWindow) Unlock() { -} - -func (w *TestWindow) SetStyle(style *nstyle.Style) { - w.ctx.Style = *style - w.ctx.Style.Defaults() -} - -func (w *TestWindow) Style() *nstyle.Style { - return &w.ctx.Style -} - -// Update runs the update function. -func (w *TestWindow) Update() { - in := &w.ctx.Input - in.Mouse.clip = nk_null_rect - w.ctx.Update() - contextAllCommands(w.ctx) - w.ctx.Draw(w.Img) - w.ctx.Reset() -} - -func (w *TestWindow) GetPerf() bool { - return false -} - -func (w *TestWindow) SetPerf(p bool) { -} - -func (w *TestWindow) PopupOpen(title string, flags WindowFlags, rect rect.Rect, scale bool, updateFn UpdateFn) { - w.ctx.popupOpen(title, flags, rect, scale, updateFn) -} - -// Click simulates a click at point p. -// The update function will be run as many times as needed, the window will -// be drawn every time. -func (w *TestWindow) Click(button mouse.Button, p image.Point) { - if button < 0 || int(button) >= len(w.ctx.Input.Mouse.Buttons) { - return - } - - in := &w.ctx.Input - // Mouse move - in.Mouse.Pos = p - in.Mouse.Delta = in.Mouse.Pos.Sub(in.Mouse.Prev) - w.Update() - - // Button press - btn := &w.ctx.Input.Mouse.Buttons[button] - btn.ClickedPos = p - btn.Clicked = true - btn.Down = true - w.Update() - - // Button release - btn.Clicked = true - btn.Down = false - w.Update() -} - -func (w *TestWindow) Walk(fn WindowWalkFn) { - w.ctx.Walk(fn) -} - -func (w *TestWindow) Input() *Input { - return &w.ctx.Input -} - -func (w *TestWindow) ResetWindows() *DockSplit { - return w.ctx.ResetWindows() -} - -// Type simulates typing. -// The update function will be run as many times as needed, the window will -// be drawn every time. -func (w *TestWindow) Type(text string) { - w.ctx.Input.Keyboard.Text = text - w.Update() -} - -func (w *TestWindow) TypeKey(e key.Event) { - var b bytes.Buffer - w.ctx.processKeyEvent(e, &b) - w.ctx.Input.Keyboard.Text = w.ctx.Input.Keyboard.Text + b.String() - w.Update() -} diff --git a/text.go b/text.go index b9ac0c1..a51a197 100644 --- a/text.go +++ b/text.go @@ -11,117 +11,14 @@ import ( "golang.org/x/mobile/event/key" "golang.org/x/mobile/event/mouse" - "golang.org/x/image/font" - "golang.org/x/image/math/fixed" - "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" nstyle "github.com/aarzilli/nucular/style" ) -/////////////////////////////////////////////////////////////////////////////////// -// TEXT WIDGETS -/////////////////////////////////////////////////////////////////////////////////// - -const ( - tabSizeInSpaces = 8 -) - -type textWidget struct { - Padding image.Point - Background color.RGBA - Text color.RGBA -} - -func textClamp(f font.Face, text []rune, space int) []rune { - text_width := 0 - for i, ch := range text { - _, _, _, xwfixed, _ := f.Glyph(fixed.P(0, 0), ch) - xw := xwfixed.Ceil() - if text_width+xw >= space { - return text[:i] - } - text_width += xw - } - return text -} - -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) - } -} - /////////////////////////////////////////////////////////////////////////////////// // TEXT EDITOR /////////////////////////////////////////////////////////////////////////////////// @@ -1073,31 +970,6 @@ func (edit *TextEditor) SelectAll() { edit.SelectEnd = len(edit.Buffer) } -func measureRunes(f font.Face, runes []rune) int { - var advance fixed.Int26_6 - prevC := rune(-1) - for _, c := range runes { - if prevC >= 0 { - advance += f.Kern(prevC, c) - } - a, ok := f.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() -} - -func glyphAdvance(f font.Face, ch rune) int { - a, _ := f.GlyphAdvance(ch) - return a.Ceil() -} - func (edit *TextEditor) editDrawText(out *command.Buffer, style *nstyle.Edit, pos image.Point, x_margin int, text []rune, textOffset int, row_height int, f font.Face, background color.RGBA, foreground color.RGBA, is_selected bool) (posOut image.Point) { if len(text) == 0 { return pos diff --git a/text_gio.go b/text_gio.go new file mode 100644 index 0000000..0dc9d2e --- /dev/null +++ b/text_gio.go @@ -0,0 +1,163 @@ +// +build nucular_gio + +package nucular + +import ( + "image" + "image/color" + "unsafe" + + "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" + + "gioui.org/font/opentype" + gioclip "gioui.org/op/clip" + "gioui.org/text" + "gioui.org/unit" +) + +type fontFace struct { + fnt *opentype.Font + shaper *text.Shaper + 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.Layout { + if width < 0 { + width = 1e6 + } + return face.shaper.Layout(face, text.Font{}, str, text.LayoutOptions{MaxWidth: width}) +} + +func (face *fontFace) shape(txtstr text.String) gioclip.Op { + return face.shaper.Shape(face, text.Font{}, 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.Lines { + if w := txt.Lines[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.Lines[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.Lines { + w += text.Lines[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.Lines { + if line.Y+line.H >= (b.Y + b.H) { + break + } + widgetText(o, line, txtline.Text.String, &text, "LC", f) + line.Y += FontHeight(f) + 2*t.Padding.Y + } +} diff --git a/text_shiny.go b/text_shiny.go new file mode 100644 index 0000000..d297702 --- /dev/null +++ b/text_shiny.go @@ -0,0 +1,210 @@ +// +build !nucular_gio + +package nucular + +import ( + "image" + "image/color" + "strings" + "unsafe" + + "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" + + "github.com/hashicorp/golang-lru" +) + +// 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) + } +} diff --git a/util.go b/util.go index 5072c70..76f6a68 100644 --- a/util.go +++ b/util.go @@ -1,17 +1,13 @@ package nucular import ( - "image" - "strings" - + "github.com/aarzilli/nucular/font" nstyle "github.com/aarzilli/nucular/style" + "image" - "golang.org/x/image/font" "golang.org/x/mobile/event/mouse" "github.com/aarzilli/nucular/rect" - - "github.com/hashicorp/golang-lru" ) type Heading int @@ -152,59 +148,6 @@ func FontHeight(f font.Face) int { return f.Metrics().Ascent.Ceil() + f.Metrics().Descent.Ceil() } -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 := font.Drawer{Face: f} - 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 unify(a rect.Rect, b rect.Rect) (clip rect.Rect) { clip.X = max(a.X, b.X) clip.Y = max(a.Y, b.Y)