package main import ( "fmt" "io" "math" "os" "os/exec" "runtime/pprof" "strconv" "strings" "time" "unicode/utf8" "github.com/bloeys/gglm/gglm" "github.com/bloeys/nmage/engine" "github.com/bloeys/nmage/input" "github.com/bloeys/nmage/materials" "github.com/bloeys/nmage/meshes" "github.com/bloeys/nmage/renderer/rend3dgl" "github.com/bloeys/nmage/timing" nmageimgui "github.com/bloeys/nmage/ui/imgui" "github.com/bloeys/nterm/assert" "github.com/bloeys/nterm/consts" "github.com/bloeys/nterm/glyphs" "github.com/bloeys/nterm/ring" "github.com/golang/freetype/truetype" "github.com/veandco/go-sdl2/sdl" "golang.org/x/exp/constraints" "golang.org/x/image/font" ) type Settings struct { DefaultColor gglm.Vec4 StringColor gglm.Vec4 } type Cmd struct { C *exec.Cmd Stdout io.ReadCloser Stdin io.WriteCloser Stderr io.ReadCloser } var _ engine.Game = &program{} type program struct { win *engine.Window rend *rend3dgl.Rend3DGL imguiInfo nmageimgui.ImguiInfo FontSize uint32 Dpi float64 GlyphRend *glyphs.GlyphRend gridMesh *meshes.Mesh gridMat *materials.Material textBuf *ring.Buffer[byte] cmdBuf []rune cmdBufLen int64 cursorCharIndex int64 // lastCmdCharPos is the screen pos of the last cmdBuf char drawn this frame lastCmdCharPos *gglm.Vec3 scrollPos int64 scrollSpd int64 maxCharsToShow int64 maxLinesToShow int64 activeCmd *Cmd Settings *Settings } const ( subPixelX = 64 subPixelY = 64 hinting = font.HintingNone defaultCmdBufSize = 4 * 1024 defaultTextBufSize = 4 * 1024 * 1024 defaultScrollSpd = 5 ) var ( // isDrawingBounds = false drawManyLines = false drawGrid bool textToShow = "" xOff float32 = 0 yOff float32 = 0 ) func main() { // x := `Hi \x1b[31Hello \x1b[31mthere` // beforeArr, code, afterArr := nextAnsiCode([]rune(x)) // fmt.Printf("x=%s; beforeArr=%s; code=%s; afterArr=%s\n", x, string(beforeArr), string(code), string(afterArr)) // return err := engine.Init() if err != nil { panic("Failed to init engine. Err: " + err.Error()) } rend := rend3dgl.NewRend3DGL() win, err := engine.CreateOpenGLWindowCentered("nTerm", 1280, 720, engine.WindowFlags_ALLOW_HIGHDPI|engine.WindowFlags_RESIZABLE, rend) if err != nil { panic("Failed to create window. Err: " + err.Error()) } engine.SetVSync(true) p := &program{ win: win, rend: rend, imguiInfo: nmageimgui.NewImGUI(), FontSize: 40, textBuf: ring.NewBuffer[byte](defaultTextBufSize), cursorCharIndex: 0, lastCmdCharPos: gglm.NewVec3(0, 0, 0), cmdBuf: make([]rune, defaultCmdBufSize), cmdBufLen: 0, scrollSpd: defaultScrollSpd, Settings: &Settings{ DefaultColor: *gglm.NewVec4(1, 1, 1, 1), StringColor: *gglm.NewVec4(242/255.0, 244/255.0, 10/255.0, 1), }, } p.win.EventCallbacks = append(p.win.EventCallbacks, p.handleSDLEvent) //Don't flash white p.win.SDLWin.GLSwap() if consts.Mode_Debug { var pf, _ = os.Create("pprof.cpu") defer pf.Close() pprof.StartCPUProfile(pf) } engine.Run(p, p.win, p.imguiInfo) if consts.Mode_Debug { pprof.StopCPUProfile() } } func (p *program) handleSDLEvent(e sdl.Event) { switch e := e.(type) { case *sdl.TextInputEvent: p.WriteToCmdBuf([]rune(e.GetText())) case *sdl.WindowEvent: if e.Event == sdl.WINDOWEVENT_SIZE_CHANGED { p.HandleWindowResize() } } } func (p *program) Init() { dpi, _, _, err := sdl.GetDisplayDPI(0) if err != nil { panic("Failed to get display DPI. Err: " + err.Error()) } fmt.Printf("DPI: %f, font size: %d\n", dpi, p.FontSize) w, h := p.win.SDLWin.GetSize() // p.GlyphRend, err = glyphs.NewGlyphRend("./res/fonts/tajawal-regular-var.ttf", &truetype.Options{Size: float64(p.FontSize), DPI: p.Dpi, SubPixelsX: subPixelX, SubPixelsY: subPixelY, Hinting: hinting}, w, h) p.GlyphRend, err = glyphs.NewGlyphRend("./res/fonts/alm-fixed.ttf", &truetype.Options{Size: float64(p.FontSize), DPI: p.Dpi, SubPixelsX: subPixelX, SubPixelsY: subPixelY, Hinting: hinting}, w, h) if err != nil { panic("Failed to create atlas from font file. Err: " + err.Error()) } if consts.Mode_Debug { glyphs.SaveImgToPNG(p.GlyphRend.Atlas.Img, "./debug-atlas.png") } //Load resources p.gridMesh, err = meshes.NewMesh("grid", "./res/models/quad.obj", 0) if err != nil { panic(err.Error()) } p.gridMat = materials.NewMaterial("grid", "./res/shaders/grid.glsl") p.HandleWindowResize() //Set initial cursor pos p.lastCmdCharPos.SetY(p.GlyphRend.Atlas.LineHeight) } func (p *program) Update() { if input.IsQuitClicked() || input.KeyClicked(sdl.K_ESCAPE) { engine.Quit() } if consts.Mode_Debug { p.DebugUpdate() } //Font sizing oldFont := p.FontSize fontSizeChanged := false if input.KeyClicked(sdl.K_KP_PLUS) { p.FontSize += 2 fontSizeChanged = true } else if input.KeyClicked(sdl.K_KP_MINUS) { p.FontSize -= 2 fontSizeChanged = true } if fontSizeChanged { err := p.GlyphRend.SetFace(&truetype.Options{Size: float64(p.FontSize), DPI: p.Dpi, SubPixelsX: subPixelX, SubPixelsY: subPixelY, Hinting: hinting}) if err != nil { p.FontSize = oldFont fmt.Println("Failed to update font face. Err: " + err.Error()) } else { glyphs.SaveImgToPNG(p.GlyphRend.Atlas.Img, "./debug-atlas.png") fmt.Println("New font size:", p.FontSize, "; New texture size:", p.GlyphRend.Atlas.Img.Rect.Max.X) } } p.MainUpdate() } // @TODO: These probably need a mutex func (p *program) WriteToTextBuf(text []byte) { p.textBuf.Write(text...) p.scrollPos = clamp(p.textBuf.Len-p.maxCharsToShow, 0, p.textBuf.Len-1) } func (p *program) WriteToCmdBuf(text []rune) { delta := int64(len(text)) newHeadPos := p.cmdBufLen + delta if newHeadPos <= defaultCmdBufSize { copy(p.cmdBuf[p.cursorCharIndex+delta:], p.cmdBuf[p.cursorCharIndex:]) copy(p.cmdBuf[p.cursorCharIndex:], text) p.cursorCharIndex += delta p.cmdBufLen = newHeadPos return } assert.T(false, "Circular buffer not implemented for cmd buf") } var sepLinePos = gglm.NewVec3(0, 0, 0) func (p *program) MainUpdate() { // Return if input.KeyClicked(sdl.K_RETURN) || input.KeyClicked(sdl.K_KP_ENTER) { p.WriteToCmdBuf([]rune{'\n'}) p.HandleReturn() } // Cursor movement and scroll if input.KeyClicked(sdl.K_LEFT) { p.cursorCharIndex = clamp(p.cursorCharIndex-1, 0, p.cmdBufLen) } else if input.KeyClicked(sdl.K_RIGHT) { p.cursorCharIndex = clamp(p.cursorCharIndex+1, 0, p.cmdBufLen) } if input.KeyClicked(sdl.K_HOME) { p.cursorCharIndex = 0 } else if input.KeyClicked(sdl.K_END) { p.cursorCharIndex = p.cmdBufLen } if mouseWheelYNorm := -int64(input.GetMouseWheelYNorm()); mouseWheelYNorm != 0 { p.scrollPos = clamp(p.scrollPos+p.scrollSpd*mouseWheelYNorm, 0, p.textBuf.Len) } // Delete inputs // @TODO: Implement hold to delete if input.KeyClicked(sdl.K_BACKSPACE) { p.DeletePrevChar() } if input.KeyClicked(sdl.K_DELETE) { p.DeleteNextChar() } //Draw textBuf v1, v2 := p.textBuf.Views() from := clamp(p.scrollPos, 0, int64(len(v1)-1)) to := clamp(p.scrollPos+p.maxCharsToShow, 0, int64(len(v1)-1)) p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(bytesToRunes(v1[from:to]), *gglm.NewVec3(0, float32(p.GlyphRend.ScreenHeight)-p.GlyphRend.Atlas.LineHeight, 0)).Data // p.lastCmdCharPos.Data = p.GlyphRend.DrawTextOpenGLAbs(v1[from:to], gglm.NewVec3(0, float32(p.GlyphRend.ScreenHeight)-p.GlyphRend.Atlas.LineHeight, 0), &p.Settings.DefaultColor).Data if p.scrollPos >= int64(len(v1)) { from := clamp(p.scrollPos-int64(len(v1)), 0, int64(len(v2)-1)) to := clamp(p.scrollPos+p.maxCharsToShow, 0, int64(len(v2)-1)) p.lastCmdCharPos.Data = p.DrawTextAnsiCodes(bytesToRunes(v2[from:to]), *p.lastCmdCharPos).Data } sepLinePos.Data = p.lastCmdCharPos.Data //Draw cmd buf p.lastCmdCharPos.SetX(0) p.lastCmdCharPos.AddY(-p.GlyphRend.Atlas.LineHeight) p.lastCmdCharPos.Data = p.SyntaxHighlightAndDraw(p.cmdBuf[:p.cmdBufLen], *p.lastCmdCharPos).Data } func bytesToRunes(b []byte) []rune { runeCount := utf8.RuneCount(b) if runeCount == 0 { return []rune{} } // @PERF We should use a pre-allocated buffer here out := make([]rune, 0, runeCount) for { r, size := utf8.DecodeRune(b) if r == utf8.RuneError { break } out = append(out, r) b = b[size:] } return out } const ( Ansi_Fg_Black = 30 Ansi_Fg_Red = 31 Ansi_Fg_Green = 32 Ansi_Fg_Yellow = 33 Ansi_Fg_Blue = 34 Ansi_Fg_Magenta = 35 Ansi_Fg_Cyan = 36 Ansi_Fg_White = 37 Ansi_Fg_Gray = 90 Ansi_Fg_Bright_Red = 91 Ansi_Fg_Bright_Green = 92 Ansi_Fg_Bright_Yellow = 93 Ansi_Fg_Bright_Blue = 94 Ansi_Fg_Bright_Magenta = 95 Ansi_Fg_Bright_Cyan = 96 Ansi_Fg_Bright_White = 97 Ansi_Bg_Black = 40 Ansi_Bg_Red = 41 Ansi_Bg_Green = 42 Ansi_Bg_Yellow = 43 Ansi_Bg_Blue = 44 Ansi_Bg_Magenta = 45 Ansi_Bg_Cyan = 46 Ansi_Bg_White = 47 Ansi_Bg_Gray = 100 Ansi_Bg_Bright_Red = 101 Ansi_Bg_Bright_Green = 102 Ansi_Bg_Bright_Yellow = 103 Ansi_Bg_Bright_Blue = 104 Ansi_Bg_Bright_Magenta = 105 Ansi_Bg_Bright_Cyan = 106 Ansi_Bg_Bright_White = 107 ) func (p *program) DrawTextAnsiCodes(text []rune, pos gglm.Vec3) gglm.Vec3 { // const ansiEsc = '\x1b' // const ansiChar1 = '\x1b' startIndex := 0 startPos := pos.Clone() currColor := p.Settings.DefaultColor for i := 0; i < len(text); i++ { r := text[i] if r == '\n' { pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:i], &pos, &currColor).Data pos.SetX(startPos.X()) pos.AddY(-p.GlyphRend.Atlas.LineHeight) startIndex = i + 1 continue } if r != '\\' || len(text)-i < 6 { continue } if text[i+1] == 'x' && text[i+2] == '1' && text[i+3] == 'b' && text[i+4] == '[' { pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:i], &pos, &currColor).Data mIndex := -1 loopStart := i + 5 for i := loopStart; i < len(text) && i < loopStart+10; i++ { r := text[i] if r == 'm' { mIndex = i break } } codeRunes := text[i+5 : mIndex] code, err := strconv.Atoi(string(codeRunes)) if err != nil { println("Invalid code runes:", string(codeRunes)) continue } if mIndex > -1 { startIndex = mIndex + 1 } else { startIndex = i } if code == 0 { currColor = p.Settings.DefaultColor } else { currColor.Data = fgColorFromAnsiCode(code).Data } } } if startIndex < len(text) { p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:], &pos, &currColor) } return pos } func fgColorFromAnsiCode(code int) gglm.Vec4 { switch code { case Ansi_Fg_Black: return gglm.Vec4{} case Ansi_Fg_Red: return gglm.Vec4{Data: [4]float32{0.5, 0, 0, 1}} case Ansi_Fg_Green: return gglm.Vec4{Data: [4]float32{0, 0.5, 0, 1}} case Ansi_Fg_Yellow: return gglm.Vec4{Data: [4]float32{0.5, 0.5, 0, 1}} case Ansi_Fg_Blue: return gglm.Vec4{Data: [4]float32{0, 0, 0.5, 1}} case Ansi_Fg_Magenta: return gglm.Vec4{Data: [4]float32{0.5, 0, 0.5, 1}} case Ansi_Fg_Cyan: return gglm.Vec4{Data: [4]float32{0, 0.66, 0.66, 1}} case Ansi_Fg_White: return gglm.Vec4{Data: [4]float32{0.8, 0.8, 0.8, 1}} case Ansi_Fg_Gray: return gglm.Vec4{Data: [4]float32{0.5, 0.5, 0.5, 1}} case Ansi_Fg_Bright_Red: return gglm.Vec4{Data: [4]float32{1, 0, 0, 1}} case Ansi_Fg_Bright_Green: return gglm.Vec4{Data: [4]float32{0, 1, 0, 1}} case Ansi_Fg_Bright_Yellow: return gglm.Vec4{Data: [4]float32{1, 1, 0, 1}} case Ansi_Fg_Bright_Blue: return gglm.Vec4{Data: [4]float32{0, 0, 1, 1}} case Ansi_Fg_Bright_Magenta: return gglm.Vec4{Data: [4]float32{1, 0, 1, 1}} case Ansi_Fg_Bright_Cyan: return gglm.Vec4{Data: [4]float32{0, 1, 1, 1}} case Ansi_Fg_Bright_White: return gglm.Vec4{Data: [4]float32{1, 1, 1, 1}} } panic("Invalid ansi code: " + fmt.Sprint(code)) } func (p *program) SyntaxHighlightAndDraw(text []rune, pos gglm.Vec3) gglm.Vec3 { startIndex := 0 startPos := pos.Clone() currColor := &p.Settings.DefaultColor inSingleString := false inDoubleString := false for i := 0; i < len(text); i++ { r := text[i] switch r { // Text might be drawn in multiple calls, once per color for example. If the first half // of the text gets drawn and the second half has a newline, the renderer will reset the X pos // to the middle of the text not the start as it uses the start X position of the second half. // So to get correct new line handling we handle newlines here case '\n': pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:i], &pos, currColor).Data pos.SetX(startPos.X()) pos.AddY(-p.GlyphRend.Atlas.LineHeight) startIndex = i + 1 continue case '"': if inSingleString { continue } if !inDoubleString { pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:i], &pos, currColor).Data startIndex = i inDoubleString = true currColor = &p.Settings.StringColor continue } pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:i+1], &pos, currColor).Data startIndex = i + 1 inDoubleString = false currColor = &p.Settings.DefaultColor case '\'': if inDoubleString { continue } if !inSingleString { pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:i], &pos, currColor).Data startIndex = i inSingleString = true currColor = &p.Settings.StringColor continue } pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:i+1], &pos, &p.Settings.StringColor).Data startIndex = i + 1 inSingleString = false currColor = &p.Settings.DefaultColor } } if startIndex < len(text) { if inDoubleString || inSingleString { pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:], &pos, &p.Settings.StringColor).Data } else { pos.Data = p.GlyphRend.DrawTextOpenGLAbs(text[startIndex:], &pos, &p.Settings.DefaultColor).Data } } return pos } func (p *program) DeletePrevChar() { if p.cursorCharIndex == 0 || p.cmdBufLen == 0 { return } copy(p.cmdBuf[p.cursorCharIndex-1:], p.cmdBuf[p.cursorCharIndex:]) p.cmdBufLen-- p.cursorCharIndex-- } func (p *program) DeleteNextChar() { if p.cmdBufLen == 0 || p.cursorCharIndex == p.cmdBufLen { return } copy(p.cmdBuf[p.cursorCharIndex:], p.cmdBuf[p.cursorCharIndex+1:]) p.cmdBufLen-- } // @TODO: Handle double quotes not being sent properly to cmd func (p *program) HandleReturn() { cmdRunes := p.cmdBuf[:p.cmdBufLen] p.cmdBufLen = 0 p.cursorCharIndex = 0 if p.activeCmd != nil { _, err := p.activeCmd.Stdin.Write([]byte(string(cmdRunes))) if err != nil { p.PrintToTextBuf(fmt.Sprintf("Writing to stdin pipe of '%s' failed. Error: %s\n", p.activeCmd.C.Path, err.Error())) p.ClearActiveCmd() return } return } // @PERF p.WriteToTextBuf([]byte(string(cmdRunes))) cmdStr := strings.TrimSpace(string(cmdRunes)) cmdSplit := strings.Split(cmdStr, " ") cmdName := cmdSplit[0] var args []string if len(cmdSplit) >= 2 { args = cmdSplit[1:] } cmd := exec.Command(cmdName, args...) outPipe, err := cmd.StdoutPipe() if err != nil { p.PrintToTextBuf(fmt.Sprintf("Creating stdout pipe of '%s' failed. Error: %s\n", cmdName, err.Error())) return } inPipe, err := cmd.StdinPipe() if err != nil { p.PrintToTextBuf(fmt.Sprintf("Creating stdin pipe of '%s' failed. Error: %s\n", cmdName, err.Error())) return } errPipe, err := cmd.StderrPipe() if err != nil { p.PrintToTextBuf(fmt.Sprintf("Creating stderr pipe of '%s' failed. Error: %s\n", cmdName, err.Error())) return } startTime := time.Now() err = cmd.Start() if err != nil { p.PrintToTextBuf(fmt.Sprintf("Running '%s' failed. Error: %s\n", cmdName, err.Error())) return } p.activeCmd = &Cmd{ C: cmd, Stdout: outPipe, Stdin: inPipe, Stderr: errPipe, } //Stdout go func() { defer func() { fmt.Printf("Cmd '%s' took %0.2fs\n", cmdName, time.Since(startTime).Seconds()) }() defer p.ClearActiveCmd() buf := make([]byte, 1024) for p.activeCmd != nil { readBytes, err := p.activeCmd.Stdout.Read(buf) if err != nil { if err == io.EOF { break } p.PrintToTextBuf("Stdout pipe failed. Error: " + err.Error()) return } if readBytes == 0 { continue } p.PrintToTextBuf(string(buf[:readBytes])) // println("Read:", string(buf[:readBytes])) } }() //Stderr go func() { defer p.ClearActiveCmd() buf := make([]byte, 1024) for p.activeCmd != nil { readBytes, err := p.activeCmd.Stderr.Read(buf) if err != nil { if err == io.EOF { break } p.PrintToTextBuf("Stderr pipe failed. Error: " + err.Error()) return } if readBytes == 0 { continue } p.PrintToTextBuf(string(buf[:readBytes])) } }() } func (p *program) ClearActiveCmd() { if p.activeCmd == nil { return } p.activeCmd = nil } func (p *program) PrintToTextBuf(s string) { p.WriteToTextBuf([]byte(s)) } func (p *program) DrawCursor() { //Position cursor by placing it at the end of the drawn characters then walking backwards pos := p.lastCmdCharPos.Clone() p.ScreenPosToGridPos(pos) pos.AddY(p.GlyphRend.Atlas.LineHeight * 0.5) for i := clamp(p.cmdBufLen, 0, int64(len(p.cmdBuf))); i > p.cursorCharIndex; i-- { if p.cmdBuf[i] == '\n' { pos.AddY(p.GlyphRend.Atlas.LineHeight) continue } pos.AddX(-p.GlyphRend.Atlas.SpaceAdvance) } p.rend.Draw(p.gridMesh, gglm.NewTrMatId().Translate(pos).Scale(gglm.NewVec3(0.1*p.GlyphRend.Atlas.SpaceAdvance, p.GlyphRend.Atlas.LineHeight, 1)), p.gridMat) } func (p *program) ScreenPosToGridPos(screenPos *gglm.Vec3) { screenPos.SetX(screenPos.X() / p.GlyphRend.Atlas.SpaceAdvance * p.GlyphRend.Atlas.SpaceAdvance) screenPos.SetY(screenPos.Y() / p.GlyphRend.Atlas.LineHeight * p.GlyphRend.Atlas.LineHeight) } func (p *program) DebugUpdate() { //Move text var speed float32 = 1 if input.KeyDown(sdl.K_RIGHT) { xOff += speed } else if input.KeyDown(sdl.K_LEFT) { xOff -= speed } if input.KeyDown(sdl.K_UP) { yOff += speed } else if input.KeyDown(sdl.K_DOWN) { yOff -= speed } //Grid if input.KeyDown(sdl.K_LCTRL) && input.KeyClicked(sdl.K_SPACE) { drawGrid = !drawGrid } } func (p *program) Render() { defer p.GlyphRend.Draw() if consts.Mode_Debug { p.DebugRender() sizeX := float32(p.GlyphRend.ScreenWidth) p.rend.Draw(p.gridMesh, gglm.NewTrMatId().Translate(gglm.NewVec3(sizeX/2, sepLinePos.Y(), 0)).Scale(gglm.NewVec3(sizeX, 1, 1)), p.gridMat) } p.DrawCursor() } func (p *program) DebugRender() { if drawGrid { p.DrawGrid() } if len(textToShow) > 0 { str := textToShow charCount := len([]rune(str)) fps := int(timing.GetAvgFPS()) if drawManyLines { const charsPerFrame = 500_000 for i := 0; i < charsPerFrame/charCount; i++ { p.GlyphRend.DrawTextOpenGLAbsString(str, gglm.NewVec3(xOff, float32(p.GlyphRend.Atlas.LineHeight)*5+yOff, 0), &p.Settings.DefaultColor) } p.win.SDLWin.SetTitle(fmt.Sprint("FPS: ", fps, " Draws/f: ", math.Ceil(charsPerFrame/glyphs.MaxGlyphsPerBatch), " chars/f: ", charsPerFrame, " chars/s: ", fps*charsPerFrame)) } else { charsPerFrame := float64(charCount) p.GlyphRend.DrawTextOpenGLAbsString(str, gglm.NewVec3(xOff, float32(p.GlyphRend.Atlas.LineHeight)*5+yOff, 0), &p.Settings.DefaultColor) p.win.SDLWin.SetTitle(fmt.Sprint("FPS: ", fps, " Draws/f: ", math.Ceil(charsPerFrame/glyphs.MaxGlyphsPerBatch), " chars/f: ", int(charsPerFrame), " chars/s: ", fps*int(charsPerFrame))) } } } func (p *program) DrawGrid() { sizeX := float32(p.GlyphRend.ScreenWidth) sizeY := float32(p.GlyphRend.ScreenHeight) //columns adv := p.GlyphRend.Atlas.SpaceAdvance for i := 0; i < int(p.GlyphRend.ScreenWidth); i++ { p.rend.Draw(p.gridMesh, gglm.NewTrMatId().Translate(gglm.NewVec3(adv*float32(i), sizeY/2, 0)).Scale(gglm.NewVec3(1, sizeY, 1)), p.gridMat) } //rows for i := int32(0); i < p.GlyphRend.ScreenHeight; i += int32(p.GlyphRend.Atlas.LineHeight) { p.rend.Draw(p.gridMesh, gglm.NewTrMatId().Translate(gglm.NewVec3(sizeX/2, float32(i), 0)).Scale(gglm.NewVec3(sizeX, 1, 1)), p.gridMat) } } func (p *program) FrameEnd() { assert.T(p.cursorCharIndex <= p.cmdBufLen, fmt.Sprintf("Cursor char index is larger than cmdBufLen! You probablly forgot to move/reset the cursor index along with the buffer length somewhere. Cursor=%d, cmdBufLen=%d\n", p.cursorCharIndex, p.cmdBufLen)) } func (p *program) DeInit() { } func (p *program) HandleWindowResize() { w, h := p.win.SDLWin.GetSize() p.GlyphRend.SetScreenSize(w, h) projMtx := gglm.Ortho(0, float32(w), float32(h), 0, 0.1, 20) viewMtx := gglm.LookAt(gglm.NewVec3(0, 0, -10), gglm.NewVec3(0, 0, 0), gglm.NewVec3(0, 1, 0)) p.gridMat.SetUnifMat4("projViewMat", &projMtx.Mul(viewMtx).Mat4) // We show a bit more than calculated to be safe against showing empty space p.maxLinesToShow = int64(CeilF32(float32(h)/float32(p.GlyphRend.Atlas.LineHeight)) * 1.25) p.maxCharsToShow = int64(CeilF32(float32(w)/float32(p.GlyphRend.Atlas.SpaceAdvance))*1.25) * p.maxLinesToShow } func FloorF32(x float32) float32 { return float32(math.Floor(float64(x))) } func CeilF32(x float32) float32 { return float32(math.Ceil(float64(x))) } func clamp[T constraints.Ordered](x, min, max T) T { if max < min { min, max = max, min } if x < min { return min } if x > max { return max } return x }