mirror of https://github.com/liamg/aminal.git
742 lines
18 KiB
Go
742 lines
18 KiB
Go
package gui
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"image/png"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/go-gl/gl/all-core/gl"
|
|
"github.com/go-gl/glfw/v3.3/glfw"
|
|
"github.com/kbinani/screenshot"
|
|
"github.com/liamg/aminal/buffer"
|
|
"github.com/liamg/aminal/config"
|
|
"github.com/liamg/aminal/platform"
|
|
"github.com/liamg/aminal/terminal"
|
|
"github.com/liamg/aminal/version"
|
|
"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
|
|
appliedWidth int
|
|
appliedHeight int
|
|
resizeCache *ResizeCache // resize cache formed by resizeToTerminal()
|
|
dpiScale float32
|
|
fontMap *FontMap
|
|
fontScale float32
|
|
renderer *OpenGLRenderer
|
|
colourAttr uint32
|
|
mouseDown bool
|
|
mouseDownModifier glfw.ModifierKey
|
|
overlay overlay
|
|
terminalAlpha float32
|
|
showDebugInfo bool
|
|
keyboardShortcuts map[config.UserAction]*config.KeyCombination
|
|
resizeLock *sync.Mutex
|
|
handCursor *glfw.Cursor
|
|
arrowCursor *glfw.Cursor
|
|
defaultCell *buffer.Cell
|
|
|
|
prevLeftClickX uint16
|
|
prevLeftClickY uint16
|
|
prevMotionTX int
|
|
prevMotionTY int
|
|
leftClickTime time.Time
|
|
leftClickCount int // number of clicks in a serie - single click, double click, or triple click
|
|
mouseMovedAfterSelectionStarted bool
|
|
internalResize bool
|
|
}
|
|
|
|
func Min(x, y int) int {
|
|
if x < y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|
|
|
|
func Max(x, y int) int {
|
|
if x > y {
|
|
return x
|
|
}
|
|
return y
|
|
}
|
|
|
|
type ResizeCache struct {
|
|
Width int
|
|
Height int
|
|
Cols uint
|
|
Rows uint
|
|
}
|
|
|
|
func (g *GUI) GetMonitor() *glfw.Monitor {
|
|
if g.window == nil {
|
|
panic("to determine current monitor the window must be set")
|
|
}
|
|
monitors := glfw.GetMonitors()
|
|
|
|
if len(monitors) == 1 {
|
|
return glfw.GetPrimaryMonitor()
|
|
}
|
|
|
|
x, y := g.window.GetPos()
|
|
w, h := g.window.GetSize()
|
|
var currentMonitor *glfw.Monitor
|
|
bestMatch := 0
|
|
|
|
for _, monitor := range monitors {
|
|
mode := monitor.GetVideoMode()
|
|
mx, my := monitor.GetPos()
|
|
overlap := Max(0, Min(x+w, mx+mode.Width)-Max(x, mx)) *
|
|
Max(0, Min(y+h, my+mode.Height)-Max(y, my))
|
|
if bestMatch < overlap {
|
|
bestMatch = overlap
|
|
currentMonitor = monitor
|
|
}
|
|
}
|
|
|
|
if currentMonitor == nil {
|
|
// Monitor couldn't be found (xrandr scaling?) - default to primary
|
|
return glfw.GetPrimaryMonitor()
|
|
}
|
|
|
|
return currentMonitor
|
|
}
|
|
|
|
// SetDPIScale sets the GUI DPI scale from user configuration (if set)
|
|
// or by calculating it from the monitor's configuration and size.
|
|
func (g *GUI) SetDPIScale() {
|
|
if g.config.DPIScale > 0 {
|
|
g.dpiScale = g.config.DPIScale
|
|
return
|
|
}
|
|
g.dpiScale = g.calculateDpiScale()
|
|
}
|
|
|
|
// calculateDpiScale determines the DPI scale in comparison with "standard" monitor's DPI values.
|
|
func (g *GUI) calculateDpiScale() float32 {
|
|
const standardDpi = 96
|
|
const mmPerInch = 25.4
|
|
|
|
m := g.GetMonitor()
|
|
widthMM, _ := m.GetPhysicalSize()
|
|
if widthMM == 0 {
|
|
return 1.0
|
|
}
|
|
|
|
monitorDpi := float32(m.GetVideoMode().Width) / (float32(widthMM) / mmPerInch)
|
|
return monitorDpi / standardDpi
|
|
}
|
|
|
|
func New(config *config.Config, terminal *terminal.Terminal, logger *zap.SugaredLogger) (*GUI, error) {
|
|
shortcuts, err := config.KeyMapping.GenerateActionMap()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &GUI{
|
|
config: config,
|
|
logger: logger,
|
|
width: 800,
|
|
height: 600,
|
|
appliedWidth: 0,
|
|
appliedHeight: 0,
|
|
dpiScale: 1,
|
|
terminal: terminal,
|
|
fontScale: 10.0,
|
|
terminalAlpha: 1,
|
|
keyboardShortcuts: shortcuts,
|
|
resizeLock: &sync.Mutex{},
|
|
internalResize: false,
|
|
}, nil
|
|
}
|
|
|
|
// inspired by https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl
|
|
|
|
func (gui *GUI) scale() float32 {
|
|
pw, _ := gui.window.GetFramebufferSize()
|
|
ww, _ := gui.window.GetSize()
|
|
return float32(ww) / float32(pw)
|
|
}
|
|
|
|
// can only be called on OS thread
|
|
func (gui *GUI) resizeToTerminal(newCols uint, newRows uint) {
|
|
if gui.window.GetAttrib(glfw.Iconified) != 0 {
|
|
return
|
|
}
|
|
|
|
gui.resizeLock.Lock()
|
|
defer gui.resizeLock.Unlock()
|
|
|
|
cols, rows := gui.renderer.GetTermSize()
|
|
if cols == newCols && rows == newRows {
|
|
return
|
|
}
|
|
|
|
gui.logger.Debugf("Initiating GUI resize to columns=%d rows=%d", newCols, newRows)
|
|
|
|
gui.logger.Debugf("Calculating size...")
|
|
width, height := gui.renderer.GetRectangleSize(newCols, newRows)
|
|
|
|
roundedWidth := int(math.Ceil(float64(width)))
|
|
roundedHeight := int(math.Ceil(float64(height)))
|
|
|
|
gui.resizeCache = &ResizeCache{roundedWidth, roundedHeight, newCols, newRows}
|
|
|
|
gui.logger.Debugf("Resizing window to %dx%d", roundedWidth, roundedHeight)
|
|
gui.internalResize = true
|
|
gui.window.SetSize(roundedWidth, roundedHeight) // will trigger resize()
|
|
gui.internalResize = false
|
|
}
|
|
|
|
func (gui *GUI) generateDefaultCell(reverse bool) {
|
|
color := gui.config.ColourScheme.Background
|
|
if reverse {
|
|
color = gui.config.ColourScheme.Foreground
|
|
}
|
|
cell := buffer.NewBackgroundCell(color)
|
|
gui.renderer.backgroundColour = color
|
|
gui.defaultCell = &cell
|
|
gl.ClearColor(
|
|
color[0],
|
|
color[1],
|
|
color[2],
|
|
1.0,
|
|
)
|
|
}
|
|
|
|
func (gui *GUI) getCursorBg(cell *buffer.Cell) (bg [3]float32) {
|
|
if gui.config.ColourScheme.Cursor != cell.Bg() {
|
|
bg = gui.config.ColourScheme.Cursor
|
|
} else {
|
|
bg = cell.Fg()
|
|
}
|
|
return bg
|
|
}
|
|
|
|
func (gui *GUI) getCursorFg(cell *buffer.Cell) (fg [3]float32) {
|
|
fg = cell.Bg()
|
|
return fg
|
|
}
|
|
|
|
// can only be called on OS thread
|
|
func (gui *GUI) resize(w *glfw.Window, width int, height int) {
|
|
if gui.window.GetAttrib(glfw.Iconified) != 0 {
|
|
return
|
|
}
|
|
|
|
if gui.appliedWidth == width && gui.appliedHeight == height {
|
|
return
|
|
}
|
|
|
|
if gui.internalResize == false {
|
|
gui.resizeLock.Lock()
|
|
defer gui.resizeLock.Unlock()
|
|
}
|
|
|
|
gui.logger.Debugf("Initiating GUI resize to %dx%d", width, height)
|
|
|
|
gui.width = width
|
|
gui.height = height
|
|
gui.appliedWidth = width
|
|
gui.appliedHeight = height
|
|
|
|
gui.logger.Debugf("Updating font resolutions...")
|
|
gui.loadFonts()
|
|
|
|
gui.logger.Debugf("Setting renderer area...")
|
|
gui.renderer.SetArea(0, 0, gui.width, gui.height)
|
|
|
|
if gui.resizeCache != nil && gui.resizeCache.Width == width && gui.resizeCache.Height == height {
|
|
gui.logger.Debugf("No need to resize internal terminal!")
|
|
} else {
|
|
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.resizeCache = nil
|
|
|
|
gui.logger.Debugf("Setting viewport size...")
|
|
gl.Viewport(0, 0, int32(gui.width), int32(gui.height))
|
|
|
|
gui.terminal.SetCharSize(gui.renderer.cellWidth, gui.renderer.cellHeight)
|
|
|
|
gui.logger.Debugf("Resize complete!")
|
|
|
|
gui.redraw()
|
|
gui.window.SwapBuffers()
|
|
}
|
|
|
|
func (gui *GUI) getTermSize() (uint, uint) {
|
|
if gui.renderer == nil {
|
|
return 0, 0
|
|
}
|
|
return gui.renderer.GetTermSize()
|
|
}
|
|
|
|
func (gui *GUI) Close() {
|
|
gui.window.SetShouldClose(true)
|
|
}
|
|
|
|
func (gui *GUI) Render() error {
|
|
gui.logger.Debugf("Creating window...")
|
|
var err error
|
|
gui.window, err = gui.createWindow()
|
|
gui.SetDPIScale()
|
|
gui.window.SetSize(int(float32(gui.width)*gui.dpiScale),
|
|
int(float32(gui.height)*gui.dpiScale))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to create window: %s", err)
|
|
}
|
|
defer glfw.Terminate()
|
|
|
|
gui.logger.Debugf("Initialising OpenGL and creating program...")
|
|
program, err := gui.createProgram()
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to initialise OpenGL: %s", err)
|
|
}
|
|
|
|
gui.colourAttr = uint32(gl.GetAttribLocation(program, gl.Str("inColour\x00")))
|
|
gl.BindFragDataLocation(program, 0, gl.Str("outColour\x00"))
|
|
|
|
gui.logger.Debugf("Loading font...")
|
|
if err := gui.loadFonts(); err != nil {
|
|
return fmt.Errorf("Failed to load font: %s", err)
|
|
}
|
|
|
|
titleChan := make(chan bool, 1)
|
|
resizeChan := make(chan bool, 1)
|
|
reverseChan := make(chan bool, 1)
|
|
|
|
gui.renderer = NewOpenGLRenderer(gui.config, gui.fontMap, 0, 0, gui.width, gui.height, gui.colourAttr, program)
|
|
|
|
gui.window.SetFramebufferSizeCallback(gui.resize)
|
|
gui.window.SetKeyCallback(gui.key)
|
|
gui.window.SetCharCallback(gui.char)
|
|
gui.window.SetScrollCallback(gui.glfwScrollCallback)
|
|
gui.window.SetMouseButtonCallback(gui.mouseButtonCallback)
|
|
gui.window.SetCursorPosCallback(gui.mouseMoveCallback)
|
|
gui.window.SetRefreshCallback(func(w *glfw.Window) {
|
|
gui.terminal.SetDirty()
|
|
})
|
|
gui.window.SetFocusCallback(func(w *glfw.Window, focused bool) {
|
|
if focused {
|
|
gui.terminal.SetDirty()
|
|
}
|
|
})
|
|
gui.window.SetPosCallback(gui.windowPosChangeCallback)
|
|
glfw.SetMonitorCallback(gui.monitorChangeCallback)
|
|
|
|
gui.generateDefaultCell(false)
|
|
|
|
{
|
|
w, h := gui.window.GetFramebufferSize()
|
|
gui.resize(gui.window, w, h)
|
|
}
|
|
|
|
gui.logger.Debugf("Starting pty read handling...")
|
|
|
|
go func() {
|
|
err := gui.terminal.Read()
|
|
if err != nil {
|
|
gui.logger.Errorf("Read from pty failed: %s", err)
|
|
}
|
|
gui.Close()
|
|
}()
|
|
|
|
gui.logger.Debugf("Starting render...")
|
|
|
|
gl.UseProgram(program)
|
|
|
|
// stop smoothing fonts
|
|
gl.Disable(gl.DEPTH_TEST)
|
|
gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
|
|
|
gui.terminal.AttachTitleChangeHandler(titleChan)
|
|
gui.terminal.AttachResizeHandler(resizeChan)
|
|
gui.terminal.AttachReverseHandler(reverseChan)
|
|
|
|
ticker := time.NewTicker(time.Second)
|
|
defer ticker.Stop()
|
|
|
|
go func() {
|
|
for {
|
|
<-ticker.C
|
|
gui.logger.Sync()
|
|
}
|
|
}()
|
|
|
|
gui.terminal.SetProgram(program)
|
|
|
|
latestVersion := ""
|
|
|
|
go func() {
|
|
r, err := version.GetNewerRelease()
|
|
if err == nil && r != nil {
|
|
latestVersion = r.TagName
|
|
gui.terminal.SetDirty()
|
|
}
|
|
}()
|
|
|
|
startTime := time.Now()
|
|
showMessage := true
|
|
|
|
for !gui.window.ShouldClose() {
|
|
|
|
forceRedraw := false
|
|
|
|
select {
|
|
case <-titleChan:
|
|
gui.window.SetTitle(gui.terminal.GetTitle())
|
|
case <-resizeChan:
|
|
cols, rows := gui.terminal.GetSize()
|
|
gui.resizeToTerminal(uint(cols), uint(rows))
|
|
case reverse := <-reverseChan:
|
|
gui.generateDefaultCell(reverse)
|
|
forceRedraw = true
|
|
default:
|
|
// this is more efficient than glfw.PollEvents()
|
|
glfw.WaitEventsTimeout(0.02) // up to 50fps on no input, otherwise higher
|
|
}
|
|
|
|
if gui.terminal.CheckDirty() || forceRedraw {
|
|
|
|
gui.redraw()
|
|
|
|
if gui.showDebugInfo {
|
|
gui.textbox(2, 2, fmt.Sprintf(`Cursor: %d,%d
|
|
View Size: %d,%d
|
|
Buffer Size: %d lines
|
|
`,
|
|
gui.terminal.GetLogicalCursorX(),
|
|
gui.terminal.GetLogicalCursorY(),
|
|
gui.terminal.ActiveBuffer().ViewWidth(),
|
|
gui.terminal.ActiveBuffer().ViewHeight(),
|
|
gui.terminal.ActiveBuffer().Height(),
|
|
),
|
|
[3]float32{1, 1, 1},
|
|
[3]float32{0.8, 0, 0},
|
|
)
|
|
}
|
|
|
|
if showMessage {
|
|
if latestVersion != "" && time.Since(startTime) < time.Second*10 && gui.terminal.ActiveBuffer().RawLine() == 0 {
|
|
time.AfterFunc(time.Second, gui.terminal.SetDirty)
|
|
_, h := gui.terminal.GetSize()
|
|
var msg string
|
|
if version.Version == "" {
|
|
msg = "You are using a development build of Aminal."
|
|
} else {
|
|
msg = fmt.Sprintf("Version %s of Aminal is now available.", strings.Replace(latestVersion, "v", "", -1))
|
|
}
|
|
gui.textbox(
|
|
2,
|
|
uint16(h-3),
|
|
fmt.Sprintf("%s (%d)", msg, 10-int(time.Since(startTime).Seconds())),
|
|
[3]float32{1, 1, 1},
|
|
[3]float32{0, 0.5, 0},
|
|
)
|
|
} else {
|
|
showMessage = false
|
|
}
|
|
}
|
|
|
|
gui.SwapBuffers()
|
|
}
|
|
|
|
}
|
|
|
|
gui.logger.Debugf("Stopping render...")
|
|
return nil
|
|
}
|
|
|
|
func (gui *GUI) redraw() {
|
|
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT)
|
|
lines := gui.terminal.GetVisibleLines()
|
|
lineCount := int(gui.terminal.ActiveBuffer().ViewHeight())
|
|
colCount := int(gui.terminal.ActiveBuffer().ViewWidth())
|
|
cx := uint(gui.terminal.GetLogicalCursorX())
|
|
cy := uint(gui.terminal.GetLogicalCursorY()) + uint(gui.terminal.GetScrollOffset())
|
|
var colour *config.Colour
|
|
for y := 0; y < lineCount; y++ {
|
|
if y < len(lines) {
|
|
cells := lines[y].Cells()
|
|
for x := 0; x < colCount; x++ {
|
|
|
|
cursor := false
|
|
if gui.terminal.Modes().ShowCursor {
|
|
cursor = cx == uint(x) && cy == uint(y)
|
|
}
|
|
|
|
if gui.terminal.ActiveBuffer().InSelection(uint16(x), uint16(y)) {
|
|
colour = &gui.config.ColourScheme.Selection
|
|
} else {
|
|
colour = nil
|
|
}
|
|
|
|
cell := gui.defaultCell
|
|
if colour != nil || cursor || x < len(cells) {
|
|
|
|
if x < len(cells) {
|
|
cell = &cells[x]
|
|
if cell.Image() != nil {
|
|
gui.renderer.DrawCellImage(*cell, uint(x), uint(y))
|
|
continue
|
|
}
|
|
}
|
|
|
|
if cursor {
|
|
var bgColour config.Colour = gui.getCursorBg(cell)
|
|
colour = &bgColour
|
|
}
|
|
|
|
gui.renderer.DrawCellBg(*cell, uint(x), uint(y), colour, false)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
for y := 0; y < lineCount; y++ {
|
|
if y < len(lines) {
|
|
|
|
var builder strings.Builder
|
|
bold := false
|
|
dim := false
|
|
col := 0
|
|
colour := [3]float32{0, 0, 0}
|
|
cells := lines[y].Cells()
|
|
|
|
for x := 0; x < colCount; x++ {
|
|
if x < len(cells) {
|
|
cell := cells[x]
|
|
|
|
cursor := false
|
|
if gui.terminal.Modes().ShowCursor {
|
|
cursor = cx == uint(x) && cy == uint(y)
|
|
}
|
|
|
|
var newFg [3]float32
|
|
if cursor {
|
|
newFg = gui.getCursorFg(&cell)
|
|
} else {
|
|
newFg = cell.Fg()
|
|
}
|
|
|
|
if builder.Len() > 0 && (cell.Attr().Dim != dim || cell.Attr().Bold != bold || colour != newFg) {
|
|
var alpha float32 = 1.0
|
|
if dim {
|
|
alpha = 0.5
|
|
}
|
|
gui.renderer.DrawCellText(builder.String(), uint(col), uint(y), alpha, colour, bold)
|
|
col = x
|
|
builder.Reset()
|
|
}
|
|
dim = cell.Attr().Dim
|
|
colour = newFg
|
|
bold = cell.Attr().Bold
|
|
r := cell.Rune()
|
|
if r == 0 {
|
|
r = ' '
|
|
}
|
|
builder.WriteRune(r)
|
|
}
|
|
}
|
|
if builder.Len() > 0 {
|
|
var alpha float32 = 1.0
|
|
if dim {
|
|
alpha = 0.5
|
|
}
|
|
gui.renderer.DrawCellText(builder.String(), uint(col), uint(y), alpha, colour, bold)
|
|
}
|
|
}
|
|
}
|
|
// underlines
|
|
for y := 0; y < lineCount; y++ {
|
|
if y < len(lines) {
|
|
|
|
span := 0
|
|
colour := [3]float32{0, 0, 0}
|
|
cells := lines[y].Cells()
|
|
|
|
var x int
|
|
|
|
for x = 0; x < colCount && x < len(cells); x++ {
|
|
cell := cells[x]
|
|
if span > 0 && (!cell.Attr().Underline || colour != cell.Fg()) {
|
|
gui.renderer.DrawUnderline(span, uint(x-span), uint(y), colour)
|
|
span = 0
|
|
}
|
|
|
|
colour = cell.Fg()
|
|
if cell.Attr().Underline {
|
|
span++
|
|
}
|
|
}
|
|
if span > 0 {
|
|
gui.renderer.DrawUnderline(span, uint(x-span), uint(y), colour)
|
|
}
|
|
}
|
|
}
|
|
gui.renderOverlay()
|
|
}
|
|
|
|
func (gui *GUI) createWindow() (*glfw.Window, error) {
|
|
if err := glfw.Init(); err != nil {
|
|
return nil, fmt.Errorf("failed to initialise GLFW: %s", err)
|
|
}
|
|
|
|
glfw.WindowHint(glfw.Resizable, glfw.True)
|
|
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
|
|
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
|
|
|
|
versions := [][2]int{
|
|
{4, 6},
|
|
{4, 5},
|
|
{4, 4},
|
|
{4, 3},
|
|
{4, 2},
|
|
{4, 1},
|
|
{4, 0},
|
|
{3, 3},
|
|
{3, 2},
|
|
}
|
|
|
|
var window *glfw.Window
|
|
|
|
for _, v := range versions {
|
|
var err error
|
|
window, err = gui.createWindowWithOpenGLVersion(v[0], v[1])
|
|
if err == nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
if window == nil {
|
|
return nil, fmt.Errorf("failed to create window, please update your graphics drivers and try again")
|
|
}
|
|
|
|
window.SetSizeLimits(int(300*gui.dpiScale), int(150*gui.dpiScale), 10000, 10000)
|
|
window.MakeContextCurrent()
|
|
window.Show()
|
|
window.Focus()
|
|
|
|
return window, nil
|
|
}
|
|
|
|
func (gui *GUI) createWindowWithOpenGLVersion(major int, minor int) (*glfw.Window, error) {
|
|
glfw.WindowHint(glfw.ContextVersionMajor, major)
|
|
glfw.WindowHint(glfw.ContextVersionMinor, minor)
|
|
|
|
window, err := glfw.CreateWindow(int(float32(gui.width)*gui.dpiScale),
|
|
int(float32(gui.height)*gui.dpiScale), "Aminal", nil, nil)
|
|
if err != nil {
|
|
e := err.Error()
|
|
if i := strings.Index(e, ", got version "); i > -1 {
|
|
v := strings.Split(strings.TrimSpace(e[i+14:]), ".")
|
|
if len(v) == 2 {
|
|
maj, mjErr := strconv.Atoi(v[0])
|
|
if mjErr == nil {
|
|
if min, miErr := strconv.Atoi(v[1]); miErr == nil {
|
|
return gui.createWindowWithOpenGLVersion(maj, min)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("failed to create window using OpenGL v%d.%d: %s", major, minor, err)
|
|
}
|
|
|
|
return window, nil
|
|
}
|
|
|
|
func (gui *GUI) onDebugMessage(source uint32, gltype uint32, id uint32, severity uint32, length int32, message string, userParam unsafe.Pointer) {
|
|
gui.logger.Infof("GL debug message: %s", message)
|
|
}
|
|
|
|
// initOpenGL initializes OpenGL and returns an intiialized program.
|
|
func (gui *GUI) createProgram() (uint32, error) {
|
|
if err := gl.Init(); err != nil {
|
|
return 0, fmt.Errorf("failed to initialise OpenGL: %s", err)
|
|
}
|
|
gui.logger.Infof("OpenGL version %s", gl.GoStr(gl.GetString(gl.VERSION)))
|
|
|
|
if gui.config.DebugMode {
|
|
// This allows to catch some OpenGL errors
|
|
gl.DebugMessageCallback(gui.onDebugMessage, nil)
|
|
gl.Enable(gl.DEBUG_OUTPUT)
|
|
}
|
|
|
|
gui.logger.Debugf("Compiling shaders...")
|
|
|
|
vertexShader, err := compileShader(vertexShaderSource, gl.VERTEX_SHADER)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
fragmentShader, err := compileShader(fragmentShaderSource, gl.FRAGMENT_SHADER)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
prog := gl.CreateProgram()
|
|
gl.AttachShader(prog, vertexShader)
|
|
gl.AttachShader(prog, fragmentShader)
|
|
gl.LinkProgram(prog)
|
|
|
|
gl.DeleteShader(vertexShader)
|
|
gl.DeleteShader(fragmentShader)
|
|
|
|
return prog, nil
|
|
}
|
|
|
|
func (gui *GUI) launchTarget(target string) {
|
|
err := platform.LaunchTarget(target)
|
|
if err != nil {
|
|
gui.logger.Errorf("Failed to launch target %s: %s", target, err)
|
|
}
|
|
}
|
|
|
|
func (gui *GUI) SwapBuffers() {
|
|
UpdateNSGLContext(gui.window)
|
|
gui.window.SwapBuffers()
|
|
}
|
|
|
|
func (gui *GUI) Screenshot(path string) {
|
|
x, y := gui.window.GetPos()
|
|
w, h := gui.window.GetSize()
|
|
|
|
img, err := screenshot.CaptureRect(image.Rectangle{
|
|
Min: image.Point{X: x, Y: y},
|
|
Max: image.Point{X: x + w, Y: y + h},
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
file, _ := os.Create(path)
|
|
defer file.Close()
|
|
png.Encode(file, img)
|
|
}
|
|
|
|
func (gui *GUI) windowPosChangeCallback(w *glfw.Window, xpos int, ypos int) {
|
|
gui.SetDPIScale()
|
|
}
|
|
|
|
func (gui *GUI) monitorChangeCallback(_ *glfw.Monitor, _ glfw.PeripheralEvent) {
|
|
gui.SetDPIScale()
|
|
}
|