efficiency, introduced resize bug though :/

This commit is contained in:
Liam Galvin 2018-08-24 17:32:08 +01:00
parent bb38d22a60
commit 88528fc6b0
11 changed files with 313 additions and 230 deletions

View File

@ -20,7 +20,7 @@ Ensure you have your latest graphics card drivers installed before use.
## What isn't supported?
- Suspend/Continue (^S, ^Q). This is archaic bullshit that annoys more people than it helps. Basically:
- Suspend/Continue (\^S, \^Q). This is archaic bullshit that annoys more people than it helps. Basically:q
<span style="display:block;text-align:center">
![Overheating](https://imgs.xkcd.com/comics/workflow.png)

View File

@ -1,9 +1,8 @@
package buffer
type Cell struct {
r rune
attr CellAttributes
hasContent bool
r rune
attr CellAttributes
}
type CellAttributes struct {
@ -43,5 +42,12 @@ func (cell *Cell) erase() {
func (cell *Cell) setRune(r rune) {
cell.r = r
cell.hasContent = r > 0
}
func NewBackgroundCell(colour [3]float32) Cell {
return Cell{
attr: CellAttributes{
BgColour: colour,
},
}
}

View File

@ -1,18 +0,0 @@
package buffer
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSetRune(t *testing.T) {
cell := newCell()
assert.False(t, cell.hasContent)
cell.setRune('X')
assert.True(t, cell.hasContent)
assert.Equal(t, 'X', cell.r)
cell.setRune('Y')
assert.True(t, cell.hasContent)
assert.Equal(t, 'Y', cell.r)
}

View File

@ -9,34 +9,37 @@ import (
"github.com/go-gl/gl/all-core/gl"
"github.com/go-gl/glfw/v3.2/glfw"
"gitlab.com/liamg/raft/buffer"
"gitlab.com/liamg/raft/config"
"gitlab.com/liamg/raft/terminal"
"go.uber.org/zap"
)
type GUI struct {
window *glfw.Window
logger *zap.SugaredLogger
config *config.Config
terminal *terminal.Terminal
width int //window width in pixels
height int //window height in pixels
font *glfont.Font
fontScale int32
renderer Renderer
colourAttr uint32
window *glfw.Window
logger *zap.SugaredLogger
config *config.Config
terminal *terminal.Terminal
width int //window width in pixels
height int //window height in pixels
font *glfont.Font
fontScale int32
renderer Renderer
colourAttr uint32
renderState *RenderState
}
func New(config *config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) *GUI {
//logger.
return &GUI{
config: config,
logger: logger,
width: 600,
height: 300,
terminal: terminal,
fontScale: 15.0,
config: config,
logger: logger,
width: 600,
height: 300,
terminal: terminal,
fontScale: 15.0,
renderState: NewRenderState(),
}
}
@ -45,25 +48,39 @@ func New(config *config.Config, terminal *terminal.Terminal, logger *zap.Sugared
// can only be called on OS thread
func (gui *GUI) resize(w *glfw.Window, width int, height int) {
gui.logger.Debugf("GUI resize to %dx%d", width, height)
if width == gui.width && height == gui.height {
//return
}
gui.logger.Debugf("Initiating GUI resize to %dx%d", width, height)
gui.width = width
gui.height = height
gui.logger.Debugf("Updating font resolution...")
if gui.font != nil {
gui.font.UpdateResolution((width), (height))
}
gl.Viewport(0, 0, int32(gui.width), int32(gui.height))
gui.logger.Debugf("Setting renderer area...")
gui.renderer.SetArea(0, 0, gui.width, gui.height)
gui.logger.Debugf("Calculating size in cols/rows...")
cols, rows := gui.renderer.GetTermSize()
gui.logger.Debugf("Resizing internal terminal...")
if err := gui.terminal.SetSize(cols, rows); err != nil {
gui.logger.Errorf("Failed to resize terminal to %d cols, %d rows: %s", cols, rows, err)
}
gui.logger.Debugf("Resetting render state...")
gui.renderState.Reset()
gui.logger.Debugf("Setting viewport size...")
gl.Viewport(0, 0, int32(gui.width), int32(gui.height))
gui.logger.Debugf("Resize complete!")
}
func (gui *GUI) glfwScrollCallback(w *glfw.Window, xoff float64, yoff float64) {
@ -93,7 +110,7 @@ func (gui *GUI) Render() error {
gui.logger.Debugf("Creating window...")
var err error
gui.window, err = gui.createWindow(gui.width, gui.height)
gui.window, err = gui.createWindow(500, 300)
if err != nil {
return fmt.Errorf("Failed to create window: %s", err)
}
@ -172,37 +189,79 @@ func (gui *GUI) Render() error {
gui.terminal.AttachTitleChangeHandler(titleChan)
gui.terminal.AttachDisplayChangeHandler(changeChan)
frames := 0
frameCount := 0
fps := 0
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
dirty := true
defaultCell := buffer.NewBackgroundCell(gui.config.ColourScheme.Background)
var lastCursorX uint
var lastCursorY uint
for !gui.window.ShouldClose() {
select {
case <-changeChan:
frames = 2
case <-ticker.C:
gui.logger.Sync()
case <-changeChan:
dirty = true
case <-titleChan:
gui.window.SetTitle(gui.terminal.GetTitle())
case <-ticker.C:
fps = frameCount
frameCount = 0
default:
}
gl.UseProgram(program)
if gui.config.Rendering.AlwaysRepaint || frames > 0 {
if dirty {
gui.window.SwapBuffers()
dirty = false
}
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
if gui.terminal.CheckDirty() {
if gui.terminal.Modes().ShowCursor {
cx := uint(gui.terminal.GetLogicalCursorX())
cy := uint(gui.terminal.GetLogicalCursorY())
cy = cy + uint(gui.terminal.GetScrollOffset())
if lastCursorX != cx || lastCursorY != cy {
gui.renderState.SetDirty(lastCursorX, lastCursorY)
dirty = true
}
} else {
gui.renderState.SetDirty(lastCursorX, lastCursorY)
dirty = true
}
lines := gui.terminal.GetVisibleLines()
for y := 0; y < len(lines); y++ {
for x, cell := range lines[y].Cells() {
gui.renderer.DrawCell(cell, uint(x), uint(y))
lineCount := gui.terminal.ActiveBuffer().ViewHeight()
colCount := gui.terminal.ActiveBuffer().ViewWidth()
for y := 0; y < int(lineCount); y++ {
for x := 0; x < int(colCount); x++ {
cell := defaultCell
empty := true
if y < len(lines) {
cells := lines[y].Cells()
if x < len(cells) {
cell = cells[x]
if cell.Rune() == 0 {
cell = defaultCell
}
empty = false
}
}
if gui.renderState.RequiresRender(uint(x), uint(y), cell.Bg(), cell.Fg(), cell.Rune(), empty) {
gui.renderer.DrawCell(cell, uint(x), uint(y))
dirty = true
}
}
}
@ -210,26 +269,19 @@ func (gui *GUI) Render() error {
cx := uint(gui.terminal.GetLogicalCursorX())
cy := uint(gui.terminal.GetLogicalCursorY())
cy = cy + uint(gui.terminal.GetScrollOffset())
gui.renderer.DrawCursor(cx, cy, gui.config.ColourScheme.Cursor)
if lastCursorX != cx || lastCursorY != cy {
gui.renderer.DrawCursor(cx, cy, gui.config.ColourScheme.Cursor)
gui.renderState.SetDirty(lastCursorX, lastCursorY)
lastCursorX = cx
lastCursorY = cy
dirty = true
}
}
_ = fps
/*
gui.font.SetColor(1, 0.5, 0.5, 0.5)
fpsData := ""
if gui.config.Rendering.AlwaysRepaint {
fpsData = fmt.Sprintf("%d FPS | %d,%d", fps, gui.terminal.GetLogicalCursorX(), gui.terminal.GetLogicalCursorY())
}
gui.font.Print(10, float32(gui.height-20), 1.5, fmt.Sprintf("%s", fpsData))
*/
}
if gui.config.Rendering.AlwaysRepaint || frames > 0 {
gui.window.SwapBuffers()
frameCount++
frames--
}
//glfw.PollEvents()
glfw.WaitEventsTimeout(0.02) // up to 50fps on no input, otherwise higher
}

View File

@ -43,6 +43,7 @@ type rectangle struct {
colourAttr uint32
colour [3]float32
points []float32
prog uint32
}
func (r *OpenGLRenderer) newRectangle(x float32, y float32, colourAttr uint32) *rectangle {
@ -53,7 +54,6 @@ func (r *OpenGLRenderer) newRectangle(x float32, y float32, colourAttr uint32) *
h := r.cellHeight / float32(r.areaHeight/2)
rect := &rectangle{
colour: [3]float32{0, 0, 1},
points: []float32{
x, y, 0,
x, y + h, 0,
@ -64,24 +64,8 @@ func (r *OpenGLRenderer) newRectangle(x float32, y float32, colourAttr uint32) *
x + w, y + h, 0,
},
colourAttr: colourAttr,
prog: r.program,
}
rect.gen()
return rect
}
func (rect *rectangle) gen() {
colour := []float32{
rect.colour[0], rect.colour[1], rect.colour[2],
rect.colour[0], rect.colour[1], rect.colour[2],
rect.colour[0], rect.colour[1], rect.colour[2],
rect.colour[0], rect.colour[1], rect.colour[2],
rect.colour[0], rect.colour[1], rect.colour[2],
rect.colour[0], rect.colour[1], rect.colour[2],
}
// SHAPE
gl.GenBuffers(1, &rect.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, rect.vbo)
@ -96,22 +80,37 @@ func (rect *rectangle) gen() {
// colour
gl.GenBuffers(1, &rect.cv)
gl.BindBuffer(gl.ARRAY_BUFFER, rect.cv)
gl.BufferData(gl.ARRAY_BUFFER, len(colour)*4, gl.Ptr(colour), gl.STATIC_DRAW)
gl.EnableVertexAttribArray(rect.colourAttr)
gl.VertexAttribPointer(rect.colourAttr, 3, gl.FLOAT, false, 0, gl.PtrOffset(0))
rect.setColour([3]float32{0, 1, 0})
return rect
}
func (rect *rectangle) setColour(colour [3]float32) {
if rect.colour == colour {
return
}
rect.Free()
c := []float32{
colour[0], colour[1], colour[2],
colour[0], colour[1], colour[2],
colour[0], colour[1], colour[2],
colour[0], colour[1], colour[2],
colour[0], colour[1], colour[2],
colour[0], colour[1], colour[2],
}
gl.UseProgram(rect.prog)
gl.BindBuffer(gl.ARRAY_BUFFER, rect.cv)
gl.BufferData(gl.ARRAY_BUFFER, len(c)*4, gl.Ptr(c), gl.STATIC_DRAW)
gl.EnableVertexAttribArray(rect.colourAttr)
gl.VertexAttribPointer(rect.colourAttr, 3, gl.FLOAT, false, 0, gl.PtrOffset(0))
rect.colour = colour
rect.gen()
}
func (rect *rectangle) Free() {
gl.UseProgram(rect.prog)
gl.DeleteVertexArrays(1, &rect.vao)
gl.DeleteBuffers(1, &rect.vbo)
gl.DeleteBuffers(1, &rect.cv)
@ -159,7 +158,6 @@ func (r *OpenGLRenderer) SetFont(font *glfont.Font) { // @todo check for monospa
r.termCols = uint(math.Floor(float64(float32(r.areaWidth) / r.cellWidth)))
r.termRows = uint(math.Floor(float64(float32(r.areaHeight) / r.cellHeight)))
r.calculatePositions()
r.generateRectangles()
}
func (r *OpenGLRenderer) calculatePositions() {
@ -175,22 +173,27 @@ func (r *OpenGLRenderer) calculatePositions() {
}
}
func (r *OpenGLRenderer) generateRectangles() {
gl.UseProgram(r.program)
for line := uint(0); line < r.termRows; line++ {
for col := uint(0); col < r.termCols; col++ {
rect, ok := r.rectangles[[2]uint{col, line}]
if ok {
rect.Free()
}
// rounding to whole pixels makes everything nice
x := float32(float64((float32(col) * r.cellWidth)))
y := float32(float64((float32(line) * r.cellHeight) + (r.cellHeight)))
r.rectangles[[2]uint{col, line}] = r.newRectangle(x, y, r.colourAttr)
}
func (r *OpenGLRenderer) getRectangle(col uint, row uint) *rectangle {
if rect, ok := r.rectangles[[2]uint{col, row}]; ok {
return rect
}
return r.generateRectangle(col, row)
}
func (r *OpenGLRenderer) generateRectangle(col uint, line uint) *rectangle {
gl.UseProgram(r.program)
rect, ok := r.rectangles[[2]uint{col, line}]
if ok {
rect.Free()
}
// rounding to whole pixels makes everything nice
x := float32(float32(col) * r.cellWidth)
y := float32((float32(line) * r.cellHeight) + (r.cellHeight))
r.rectangles[[2]uint{col, line}] = r.newRectangle(x, y, r.colourAttr)
return r.rectangles[[2]uint{col, line}]
}
func (r *OpenGLRenderer) DrawCursor(col uint, row uint, colour config.Colour) {
@ -220,10 +223,6 @@ func (r *OpenGLRenderer) DrawCursor(col uint, row uint, colour config.Colour) {
func (r *OpenGLRenderer) DrawCell(cell buffer.Cell, col uint, row uint) {
if cell.Attr().Hidden || (cell.Rune() == 0x00) {
return
}
var fg [3]float32
var bg [3]float32
@ -241,18 +240,10 @@ func (r *OpenGLRenderer) DrawCell(cell buffer.Cell, col uint, row uint) {
}
gl.UseProgram(r.program)
// don't bother rendering rectangles that are the same colour as the background
if bg != r.config.ColourScheme.Background {
rect, ok := r.rectangles[[2]uint{col, row}]
if !ok {
panic(fmt.Sprintf("Missing rectangle data for cell at %d,%d", col, row))
}
rect.setColour(bg)
gl.BindVertexArray(rect.vao)
gl.DrawArrays(gl.TRIANGLE_STRIP, 0, 6)
}
rect := r.getRectangle(col, row)
rect.setColour(bg)
gl.BindVertexArray(rect.vao)
gl.DrawArrays(gl.TRIANGLES, 0, 6)
var alpha float32 = 1
if cell.Attr().Dim {

58
gui/renderstate.go Normal file
View File

@ -0,0 +1,58 @@
package gui
type RenderState struct {
cells map[[2]uint]RenderedCell
}
func NewRenderState() *RenderState {
return &RenderState{
cells: map[[2]uint]RenderedCell{},
}
}
type RenderedCell struct {
bg [3]float32
fg [3]float32
contents rune
dirty bool
}
func (rs *RenderState) Reset() {
rs.cells = map[[2]uint]RenderedCell{}
}
func (rs *RenderState) SetDirty(x uint, y uint) {
rs.cells[[2]uint{x, y}] = RenderedCell{
bg: [3]float32{0, 0, 0},
fg: [3]float32{0, 0, 0},
contents: 0,
dirty: true,
}
}
func (rs *RenderState) RequiresRender(x uint, y uint, bg [3]float32, fg [3]float32, contents rune, empty bool) bool {
state, found := rs.cells[[2]uint{x, y}]
if !found {
if empty {
//return false
}
rs.cells[[2]uint{x, y}] = RenderedCell{
bg: bg,
fg: fg,
contents: contents,
}
return true
}
if state.bg != bg || state.fg != fg || state.contents != contents || state.dirty {
rs.cells[[2]uint{x, y}] = RenderedCell{
bg: bg,
fg: fg,
contents: contents,
}
return true
}
return false
}

View File

@ -1,18 +1,36 @@
package terminal
import "fmt"
// https://www.xfree86.org/4.8.0/ctlseqs.html
// https://vt100.net/docs/vt100-ug/chapter3.html
var ansiSequenceMap = map[rune]escapeSequenceHandler{
'[': csiHandler,
0x5d: oscHandler,
'7': saveCursorHandler,
'8': restoreCursorHandler,
'D': indexHandler,
'M': reverseIndexHandler,
'[': csiHandler,
']': oscHandler,
'7': saveCursorHandler,
'8': restoreCursorHandler,
'D': indexHandler,
'M': reverseIndexHandler,
'c': swallowHandler(0), //RIS
'(': swallowHandler(1), // character set bullshit
')': swallowHandler(1), // character set bullshit
'*': swallowHandler(1), // character set bullshit
'+': swallowHandler(1), // character set bullshit
'>': swallowHandler(0), // numeric char selection //@todo
'=': swallowHandler(0), // alt char selection //@todo
}
func indexHandler(buffer chan rune, terminal *Terminal) error {
func swallowHandler(n int) func(pty chan rune, terminal *Terminal) error {
return func(pty chan rune, terminal *Terminal) error {
for i := 0; i < n; i++ {
<-pty
}
return nil
}
}
func indexHandler(pty chan rune, terminal *Terminal) error {
// @todo is thus right?
// "This sequence causes the active position to move downward one line without changing the column position. If the active position is at the bottom margin, a scroll up is performed."
if terminal.ActiveBuffer().CursorLine() == terminal.ActiveBuffer().ViewHeight()-1 {
@ -23,17 +41,17 @@ func indexHandler(buffer chan rune, terminal *Terminal) error {
return nil
}
func reverseIndexHandler(buffer chan rune, terminal *Terminal) error {
func reverseIndexHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().MovePosition(0, -1)
return nil
}
func saveCursorHandler(buffer chan rune, terminal *Terminal) error {
func saveCursorHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().SaveCursor()
return nil
}
func restoreCursorHandler(buffer chan rune, terminal *Terminal) error {
func restoreCursorHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().RestoreCursor()
return nil
}
@ -47,67 +65,5 @@ func ansiHandler(pty chan rune, terminal *Terminal) error {
return handler(pty, terminal)
}
switch b {
case 'c':
terminal.logger.Errorf("RIS not yet supported")
case '(':
b = <-pty
switch b {
case 'A': //uk @todo handle these?
//terminal.charSet = C0
case 'B': //us
//terminal.charSet = C0
}
case ')':
b = <-pty
switch b {
case 'A': //uk @todo handle these?
//terminal.charSet = C1
case 'B': //us
//terminal.charSet = C1
}
case '*':
b = <-pty
switch b {
case 'A': //uk @todo handle these?
//terminal.charSet = C2
case 'B': //us
//terminal.charSet = C2
}
case '+':
b = <-pty
switch b {
case 'A': //uk @todo handle these?
//terminal.charSet = C3
case 'B': //us
//terminal.charSet = C3
}
case '>':
// numeric char selection @todo
case '=':
//alternate char selection @todo
case '?':
pm := ""
for {
b = <-pty
switch b {
case 'h':
switch pm {
default:
terminal.logger.Errorf("Unknown private code ESC?%sh", pm)
}
case 'l':
switch pm {
default:
terminal.logger.Errorf("Unknown private code ESC?%sl", pm)
}
default:
pm += string(b)
}
}
default:
terminal.logger.Errorf("Unknown control sequence: 0x%02X [%s]", b, string(b))
}
return nil
return fmt.Errorf("Unknown ANSI control sequence byte: 0x%02X [%v]", b, string(b))
}

View File

@ -98,7 +98,7 @@ func csiSetMode(modeStr string, enabled bool, terminal *Terminal) error {
} else {
terminal.UseMainBuffer()
}
case "?1000", "?10061000": // ?10061000 seen from htop
case "?1000", "?1006;1000", "?10061000": // ?10061000 seen from htop
// enable mouse tracking
if enabled {
terminal.SetMouseMode(MouseModeVT200)
@ -153,16 +153,16 @@ func csiWindowManipulation(params []string, intermediate string, terminal *Termi
}
func csiLinePositionAbsolute(params []string, intermediate string, terminal *Terminal) error {
col := 1
row := 1
if len(params) > 0 {
var err error
col, err = strconv.Atoi(params[0])
row, err = strconv.Atoi(params[0])
if err != nil {
col = 1
row = 1
}
}
terminal.ActiveBuffer().SetPosition(uint16(col), terminal.ActiveBuffer().CursorLine())
terminal.ActiveBuffer().SetPosition(terminal.ActiveBuffer().CursorColumn(), uint16(row-1))
return nil
}

View File

@ -12,9 +12,59 @@ type TerminalCharSet int
type escapeSequenceHandler func(pty chan rune, terminal *Terminal) error
var escapeSequenceMap = map[rune]escapeSequenceHandler{
0x05: enqSequenceHandler,
0x07: bellSequenceHandler,
0x08: backspaceSequenceHandler,
0x09: tabSequenceHandler,
0x0a: newLineSequenceHandler,
0x0b: newLineSequenceHandler,
0x0c: newLineSequenceHandler,
0x0d: carriageReturnSequenceHandler,
0x0e: shiftOutSequenceHandler,
0x0f: shiftInSequenceHandler,
0x1b: ansiHandler,
}
func newLineSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().NewLine()
return nil
}
func carriageReturnSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().CarriageReturn()
return nil
}
func backspaceSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.ActiveBuffer().Backspace()
return nil
}
func bellSequenceHandler(pty chan rune, terminal *Terminal) error {
// @todo ring bell - flash red or some shit?
return nil
}
func enqSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.logger.Errorf("Received ENQ!")
return nil
}
func shiftOutSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.logger.Errorf("Received shift out")
return nil
}
func shiftInSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.logger.Errorf("Received shift in")
return nil
}
func tabSequenceHandler(pty chan rune, terminal *Terminal) error {
terminal.logger.Errorf("Received tab")
return nil
}
func (terminal *Terminal) processInput(ctx context.Context, pty chan rune) {
// https://en.wikipedia.org/wiki/ANSI_escape_code
@ -43,28 +93,8 @@ func (terminal *Terminal) processInput(ctx context.Context, pty chan rune) {
if err := handler(pty, terminal); err != nil {
terminal.logger.Errorf("Error handling escape sequence: %s", err)
}
continue
}
terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
switch b {
case 0x0a, 0x0c, 0x0b: // LF, FF, VT
terminal.ActiveBuffer().NewLine()
case 0x0d: // CR
terminal.ActiveBuffer().CarriageReturn()
case 0x08: // BS
// backspace
terminal.ActiveBuffer().Backspace()
case 0x07: // BEL
// @todo ring bell - flash red or some shit?
case 0x05: // ENQ
terminal.logger.Errorf("Received ENQ!")
case 0xe, 0xf:
terminal.logger.Errorf("Received SI/SO")
case 0x09:
terminal.logger.Errorf("Received TAB")
default:
} else {
terminal.logger.Debugf("Received character 0x%X: %q", b, string(b))
if b >= 0x20 {
terminal.ActiveBuffer().Write(b)
} else {
@ -72,5 +102,6 @@ func (terminal *Terminal) processInput(ctx context.Context, pty chan rune) {
}
}
terminal.isDirty = true
}
}

View File

@ -45,6 +45,7 @@ type Terminal struct {
resumeChan chan bool
modes Modes
mouseMode MouseMode
isDirty bool
}
type Modes struct {
@ -90,6 +91,12 @@ func New(pty *os.File, logger *zap.SugaredLogger, config *config.Config) *Termin
}
}
func (terminal *Terminal) CheckDirty() bool {
d := terminal.isDirty
terminal.isDirty = false
return d
}
func (terminal *Terminal) IsApplicationCursorKeysModeEnabled() bool {
return terminal.modes.ApplicationCursorKeys
}

View File

@ -219,7 +219,7 @@ func (f *Font) Height(scale float32, text string) float32 {
baseHeight = height
height = 0
}
//skip runes that are not in font chacter range
if int(runeIndex)-int(lowChar) > len(f.fontChar) || runeIndex < lowChar {
continue
@ -229,7 +229,7 @@ func (f *Font) Height(scale float32, text string) float32 {
ch := f.fontChar[runeIndex-lowChar]
// Now advance cursors for next glyph (note that advance is number of 1/64 pixels)
if float32(ch.height) * scale > height {
if float32(ch.height)*scale > height {
height = float32(ch.height) * scale
}