Merge branch 'develop' into hyperlinks

This commit is contained in:
rrrooommmaaa 2019-03-20 13:03:37 +03:00 committed by GitHub
commit c9086647ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1045 additions and 288 deletions

View File

@ -114,28 +114,29 @@ shell = "/bin/bash" # The shell to run for the terminal session. Default
search_url = "https://www.google.com/search?q=$QUERY" # The search engine to use for the "search selected text" action. Defaults to google. Set this to your own search url using $QUERY as the keywords to replace when searching.
max_lines = 1000 # Maximum number of lines in the terminal buffer.
copy_and_paste_with_mouse = true # Text selected with the mouse is copied to the clipboard on end selection, and is pasted on right mouse button click.
show_vertical_scrollbar = true # Whether to show the vertical scrollbar
dpi-scale = 0.0 # Override DPI scale. Defaults to 0.0 (let Aminal determine the DPI scale itself).
[colours]
cursor = "#e8dfd6"
foreground = "#e8dfd6"
background = "#021b21"
black = "#032c36"
red = "#c2454e"
green = "#7cbf9e"
yellow = "#8a7a63"
blue = "#065f73"
magenta = "#ff5879"
cyan = "#44b5b1"
light_grey = "#f2f1b9"
dark_grey = "#3e4360"
light_red = "#ef5847"
light_green = "#a2db91"
light_yellow = "#beb090"
light_blue = "#61778d"
light_magenta = "#ff99a1"
light_cyan = "#9ed9d8"
white = "#f6f6c9"
black = "#000000"
red = "#800000"
green = "#008000"
yellow = "#808000"
blue = "#000080"
magenta = "#800080"
cyan = "#008080"
light_grey = "#f2f2f2"
dark_grey = "#808080"
light_red = "#ff0000"
light_green = "#00ff00"
light_yellow = "#ffff00"
light_blue = "#0000ff"
light_magenta = "#ff00ff"
light_cyan = "#00ffff"
white = "#ffffff"
selection = "#333366" # Mouse selection background colour
[keys]

View File

@ -219,7 +219,7 @@ func (buffer *Buffer) GetSelectedText(selectionRegionMode SelectionRegionMode) s
maxX := int(buffer.terminalState.viewWidth) - 1
if row == start.Line {
minX = start.Col
} else if !line.wrapped {
} else if !line.wrapped && !line.nobreak {
builder.WriteString("\n")
}
if row == end.Line {
@ -522,6 +522,15 @@ func (buffer *Buffer) convertRawLineToViewLine(rawLine uint64) uint16 {
return uint16(int(rawLine) - (rawHeight - int(buffer.terminalState.viewHeight)))
}
func (buffer *Buffer) GetVPosition() int {
result := int(uint(buffer.Height()) - uint(buffer.ViewHeight()) - buffer.terminalState.scrollLinesFromBottom)
if result < 0 {
result = 0
}
return result
}
// Width returns the width of the buffer in columns
func (buffer *Buffer) Width() uint16 {
return buffer.terminalState.viewWidth
@ -549,7 +558,7 @@ func (buffer *Buffer) insertLine() {
if !buffer.InScrollableRegion() {
pos := buffer.RawLine()
maxLines := buffer.getMaxLines()
maxLines := buffer.GetMaxLines()
newLineCount := uint64(len(buffer.lines) + 1)
if newLineCount > maxLines {
newLineCount = maxLines
@ -645,7 +654,7 @@ func (buffer *Buffer) Index() {
if buffer.terminalState.cursorY >= buffer.ViewHeight()-1 {
buffer.lines = append(buffer.lines, newLine())
maxLines := buffer.getMaxLines()
maxLines := buffer.GetMaxLines()
if uint64(len(buffer.lines)) > maxLines {
copy(buffer.lines, buffer.lines[uint64(len(buffer.lines))-maxLines:])
buffer.lines = buffer.lines[:maxLines]
@ -699,6 +708,7 @@ func (buffer *Buffer) Write(runes ...rune) {
buffer.NewLineEx(true)
newLine := buffer.getCurrentLine()
newLine.setNoBreak(true)
if len(newLine.cells) == 0 {
newLine.Append(buffer.terminalState.DefaultCell(true))
}
@ -1119,7 +1129,7 @@ func (buffer *Buffer) ResizeView(width uint16, height uint16) {
buffer.terminalState.ResetVerticalMargins()
}
func (buffer *Buffer) getMaxLines() uint64 {
func (buffer *Buffer) GetMaxLines() uint64 {
result := buffer.terminalState.maxLines
if result < uint64(buffer.terminalState.viewHeight) {
result = uint64(buffer.terminalState.viewHeight)

View File

@ -6,12 +6,14 @@ import (
type Line struct {
wrapped bool // whether line was wrapped onto from the previous one
nobreak bool // true if no line break at the beginning of the line
cells []Cell
}
func newLine() Line {
return Line{
wrapped: false,
nobreak: false,
cells: []Cell{},
}
}
@ -45,6 +47,10 @@ func (line *Line) setWrapped(wrapped bool) {
line.wrapped = wrapped
}
func (line *Line) setNoBreak(nobreak bool) {
line.nobreak = nobreak
}
func (line *Line) String() string {
runes := []rune{}
for _, cell := range line.cells {

View File

@ -14,6 +14,7 @@ type Config struct {
SearchURL string `toml:"search_url"`
MaxLines uint64 `toml:"max_lines"`
CopyAndPasteWithMouse bool `toml:"copy_and_paste_with_mouse"`
ShowVerticalScrollbar bool `toml:"show_vertical_scrollbar"`
// Developer options.
DebugMode bool `toml:"debug"`

View File

@ -39,6 +39,7 @@ func DefaultConfig() *Config {
SearchURL: "https://www.google.com/search?q=$QUERY",
MaxLines: 1000,
CopyAndPasteWithMouse: true,
ShowVerticalScrollbar: true,
}
}

View File

@ -8,14 +8,14 @@ import (
"github.com/liamg/aminal/glfont"
)
func (gui *GUI) getPackedFont(name string) (*glfont.Font, error) {
func (gui *GUI) getPackedFont(name string, actualWidth int, actualHeight int) (*glfont.Font, error) {
box := packr.NewBox("./packed-fonts")
fontBytes, err := box.Find(name)
if err != nil {
return nil, fmt.Errorf("packaged font '%s' could not be read: %s", name, err)
}
font, err := glfont.LoadFont(bytes.NewReader(fontBytes), gui.fontScale*gui.dpiScale/gui.scale(), gui.width, gui.height)
font, err := glfont.LoadFont(bytes.NewReader(fontBytes), gui.fontScale*gui.dpiScale/gui.scale(), actualWidth, actualHeight)
if err != nil {
return nil, fmt.Errorf("font '%s' failed to load: %v", name, err)
}
@ -23,16 +23,16 @@ func (gui *GUI) getPackedFont(name string) (*glfont.Font, error) {
return font, nil
}
func (gui *GUI) loadFonts() error {
func (gui *GUI) loadFonts(actualWidth int, actualHeight int) error {
// from https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Hack
defaultFont, err := gui.getPackedFont("Hack Regular Nerd Font Complete.ttf")
defaultFont, err := gui.getPackedFont("Hack Regular Nerd Font Complete.ttf", actualWidth, actualHeight)
if err != nil {
return err
}
boldFont, err := gui.getPackedFont("Hack Bold Nerd Font Complete.ttf")
boldFont, err := gui.getPackedFont("Hack Bold Nerd Font Complete.ttf", actualWidth, actualHeight)
if err != nil {
return err
}

View File

@ -30,6 +30,18 @@ import (
const wakePeriod = time.Second / 120
const halfWakePeriod = wakePeriod / 2
const (
DefaultWindowWidth = 800
DefaultWindowHeight = 600
)
type mouseEventsHandler interface {
mouseMoveCallback(g *GUI, px float64, py float64)
mouseButtonCallback(g *GUI, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey, mouseX float64, mouseY float64)
cursorEnterCallback(g *GUI, enter bool)
isMouseInside(px float64, py float64) bool
}
type GUI struct {
window *glfw.Window
logger *zap.SugaredLogger
@ -63,8 +75,15 @@ type GUI struct {
leftClickTime time.Time
leftClickCount int // number of clicks in a serie - single click, double click, or triple click
mouseMovedAfterSelectionStarted bool
internalResize bool
selectionRegionMode buffer.SelectionRegionMode
catchedMouseHandler mouseEventsHandler
mouseCatchedOnButton glfw.MouseButton
prevMouseEventHandler mouseEventsHandler
internalResize bool
selectionRegionMode buffer.SelectionRegionMode
vScrollbar *scrollbar
mainThreadFunc chan func()
}
@ -156,24 +175,33 @@ func New(config *config.Config, terminal *terminal.Terminal, logger *zap.Sugared
}
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,
config: config,
logger: logger,
width: DefaultWindowWidth,
height: DefaultWindowHeight,
appliedWidth: 0,
appliedHeight: 0,
dpiScale: 1,
terminal: terminal,
fontScale: 10.0,
terminalAlpha: 1,
keyboardShortcuts: shortcuts,
resizeLock: &sync.Mutex{},
internalResize: false,
vScrollbar: nil,
catchedMouseHandler: nil,
mainThreadFunc: make(chan func()),
}, nil
}
func (gui *GUI) Free() {
if gui.vScrollbar != nil {
gui.vScrollbar.Free()
gui.vScrollbar = nil
}
}
// inspired by https://kylewbanks.com/blog/tutorial-opengl-with-golang-part-1-hello-opengl
func (gui *GUI) scale() float32 {
@ -208,15 +236,20 @@ func (gui *GUI) resizeToTerminal() {
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)
width, height := gui.renderer.ConvertCoordinates(newCols, newRows)
roundedWidth := int(math.Ceil(float64(width)))
roundedHeight := int(math.Ceil(float64(height)))
if gui.vScrollbar != nil {
roundedWidth += int(gui.vScrollbar.position.width())
}
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
}
@ -278,14 +311,20 @@ func (gui *GUI) resize(w *glfw.Window, width int, height int) {
gui.width = width
gui.height = height
gui.appliedWidth = width
gui.appliedHeight = height
gui.appliedWidth = gui.width
gui.appliedHeight = gui.height
vScrollbarWidth := 0
if gui.vScrollbar != nil {
gui.vScrollbar.resize(gui)
vScrollbarWidth = int(gui.vScrollbar.position.width())
}
gui.logger.Debugf("Updating font resolutions...")
gui.loadFonts()
gui.loadFonts(gui.width, gui.height)
gui.logger.Debugf("Setting renderer area...")
gui.renderer.SetArea(0, 0, gui.width, gui.height)
gui.renderer.SetArea(0, 0, gui.width-vScrollbarWidth, gui.height)
if gui.resizeCache != nil && gui.resizeCache.Width == width && gui.resizeCache.Height == height {
gui.logger.Debugf("No need to resize internal terminal!")
@ -334,18 +373,17 @@ func (gui *GUI) Close() {
}
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.SetDPIScale()
gui.window.SetSize(int(float32(gui.width)*gui.dpiScale), int(float32(gui.height)*gui.dpiScale))
gui.logger.Debugf("Initialising OpenGL and creating program...")
program, err := gui.createProgram()
if err != nil {
@ -355,8 +393,19 @@ func (gui *GUI) Render() error {
gui.colourAttr = uint32(gl.GetAttribLocation(program, gl.Str("inColour\x00")))
gl.BindFragDataLocation(program, 0, gl.Str("outColour\x00"))
vScrollbarWidth := 0
if gui.config.ShowVerticalScrollbar {
vScrollbar, err := newScrollbar()
if err != nil {
return err
}
gui.vScrollbar = vScrollbar
gui.vScrollbar.resize(gui)
vScrollbarWidth = int(gui.vScrollbar.position.width())
}
gui.logger.Debugf("Loading font...")
if err := gui.loadFonts(); err != nil {
if err := gui.loadFonts(gui.width, gui.height); err != nil {
return fmt.Errorf("Failed to load font: %s", err)
}
@ -364,14 +413,18 @@ func (gui *GUI) Render() error {
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.renderer, err = NewOpenGLRenderer(gui.config, gui.fontMap, 0, 0, gui.width-vScrollbarWidth, gui.height, gui.colourAttr, program)
if err != nil {
return err
}
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.SetMouseButtonCallback(gui.globalMouseButtonCallback)
gui.window.SetCursorPosCallback(gui.globalMouseMoveCallback)
gui.window.SetCursorEnterCallback(gui.globalCursorEnterCallback)
gui.window.SetRefreshCallback(func(w *glfw.Window) {
gui.terminal.NotifyDirty()
})
@ -404,6 +457,10 @@ func (gui *GUI) Render() error {
gui.Close()
}()
if gui.vScrollbar != nil {
gui.vScrollbar.resize(gui)
}
gui.logger.Debugf("Starting render...")
gl.UseProgram(program)
@ -412,16 +469,6 @@ func (gui *GUI) Render() error {
gl.Disable(gl.DEPTH_TEST)
gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
go func() {
for {
<-ticker.C
gui.logger.Sync()
}
}()
gui.terminal.SetProgram(program)
latestVersion := ""
@ -438,7 +485,9 @@ func (gui *GUI) Render() error {
showMessage := true
stop := make(chan struct{})
go gui.waker(stop)
var waitForWaker sync.WaitGroup
waitForWaker.Add(1)
go gui.waker(stop, &waitForWaker)
for !gui.window.ShouldClose() {
gui.redraw(true)
@ -502,9 +551,13 @@ Buffer Size: %d lines
}
}
close(stop) // Tell waker to end.
gui.logger.Debug("Stopping render...")
close(stop) // Tell waker to end...
waitForWaker.Wait() // ...and wait it to end
gui.logger.Debug("Render stopped")
gui.logger.Debugf("Stopping render...")
return nil
}
@ -513,10 +566,13 @@ Buffer Size: %d lines
// waking up the main thread when the GUI needs to be
// redrawn. Limiting is applied on wakeups to avoid excessive CPU
// usage when the terminal is being updated rapidly.
func (gui *GUI) waker(stop <-chan struct{}) {
func (gui *GUI) waker(stop <-chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
dirty := gui.terminal.Dirty()
var nextWake <-chan time.Time
var last time.Time
forLoop:
for {
select {
case <-dirty:
@ -540,7 +596,7 @@ func (gui *GUI) waker(stop <-chan struct{}) {
glfw.PostEmptyEvent()
nextWake = nil
case <-stop:
return
break forLoop
}
}
}
@ -677,7 +733,6 @@ func (gui *GUI) renderTerminalData(shouldLock bool) {
gui.renderer.DrawUnderline(span, uint(x-span), uint(y), colour)
}
}
}
// hyperlinks
for y := 0; y < lineCount; y++ {
@ -709,6 +764,7 @@ func (gui *GUI) renderTerminalData(shouldLock bool) {
}
gui.renderScrollbar()
}
func (gui *GUI) redraw(shouldLock bool) {
@ -840,18 +896,27 @@ func (gui *GUI) SwapBuffers() {
gui.window.SwapBuffers()
}
func (gui *GUI) Screenshot(path string) {
func (gui *GUI) Screenshot(path string) error {
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)
return err
}
file, err := os.Create(path)
if err != nil {
return err
}
file, _ := os.Create(path)
defer file.Close()
png.Encode(file, img)
err = png.Encode(file, img)
if err != nil {
os.Remove(path)
return err
}
return nil
}
func (gui *GUI) windowPosChangeCallback(w *glfw.Window, xpos int, ypos int) {
@ -862,6 +927,16 @@ func (gui *GUI) monitorChangeCallback(monitor *glfw.Monitor, event glfw.MonitorE
gui.SetDPIScale()
}
func (gui *GUI) renderScrollbar() {
if gui.vScrollbar != nil {
position := gui.terminal.ActiveBuffer().GetVPosition()
maxPosition := int(gui.terminal.ActiveBuffer().GetMaxLines()) - int(gui.terminal.ActiveBuffer().ViewHeight())
gui.vScrollbar.setPosition(maxPosition, position)
gui.vScrollbar.render(gui)
}
}
// Synchronously executes the argument function in the main thread.
// Does not return until f() executed!
func (gui *GUI) executeInMainThread(f func() error) error {

View File

@ -36,7 +36,86 @@ func (gui *GUI) getArrowCursor() *glfw.Cursor {
return gui.arrowCursor
}
func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) {
func (gui *GUI) scaleMouseCoordinates(px float64, py float64) (float64, float64) {
scale := float64(gui.scale())
px = px / scale
py = py / scale
return px, py
}
func (gui *GUI) globalMouseMoveCallback(w *glfw.Window, px float64, py float64) {
px, py = gui.scaleMouseCoordinates(px, py)
if gui.catchedMouseHandler != nil {
gui.catchedMouseHandler.mouseMoveCallback(gui, px, py)
} else {
if gui.isMouseInside(px, py) {
if gui.prevMouseEventHandler != gui {
if gui.prevMouseEventHandler != nil {
gui.prevMouseEventHandler.cursorEnterCallback(gui, false)
}
gui.cursorEnterCallback(gui, true)
}
gui.mouseMoveCallback(gui, px, py)
gui.prevMouseEventHandler = gui
} else if gui.vScrollbar != nil && gui.vScrollbar.isMouseInside(px, py) {
if gui.prevMouseEventHandler != gui.vScrollbar {
if gui.prevMouseEventHandler != nil {
gui.prevMouseEventHandler.cursorEnterCallback(gui, false)
}
gui.vScrollbar.cursorEnterCallback(gui, true)
}
gui.vScrollbar.mouseMoveCallback(gui, px, py)
gui.prevMouseEventHandler = gui.vScrollbar
}
}
}
func (gui *GUI) globalMouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
mouseX, mouseY := gui.scaleMouseCoordinates(w.GetCursorPos())
if gui.catchedMouseHandler != nil {
gui.catchedMouseHandler.mouseButtonCallback(gui, button, action, mod, mouseX, mouseY)
if action == glfw.Release && button == gui.mouseCatchedOnButton {
gui.catchMouse(nil, 0)
}
} else {
if gui.isMouseInside(mouseX, mouseY) {
if action == glfw.Press {
gui.catchMouse(gui, button)
}
gui.mouseButtonCallback(gui, button, action, mod, mouseX, mouseY)
} else if gui.vScrollbar != nil && gui.vScrollbar.isMouseInside(mouseX, mouseY) {
if action == glfw.Press {
gui.catchMouse(gui.vScrollbar, button)
}
gui.vScrollbar.mouseButtonCallback(gui, button, action, mod, mouseX, mouseY)
}
}
}
func (gui *GUI) globalCursorEnterCallback(w *glfw.Window, entered bool) {
if !entered {
if gui.prevMouseEventHandler != nil {
gui.prevMouseEventHandler.cursorEnterCallback(gui, false)
gui.prevMouseEventHandler = nil
}
}
}
func (gui *GUI) catchMouse(newHandler mouseEventsHandler, button glfw.MouseButton) {
gui.catchedMouseHandler = newHandler
gui.mouseCatchedOnButton = button
}
func (gui *GUI) isMouseInside(px float64, py float64) bool {
return px >= float64(gui.renderer.areaX) && px < float64(gui.renderer.areaX+gui.renderer.areaWidth) &&
py >= float64(gui.renderer.areaY) && py < float64(gui.renderer.areaY+gui.renderer.areaHeight)
}
func (gui *GUI) mouseMoveCallback(g *GUI, px float64, py float64) {
x, y := gui.convertMouseCoordinates(px, py)
@ -59,16 +138,13 @@ func (gui *GUI) mouseMoveCallback(w *glfw.Window, px float64, py float64) {
}
if url := gui.terminal.ActiveBuffer().GetURLAtPosition(x, y); url != "" {
w.SetCursor(gui.getHandCursor())
gui.window.SetCursor(gui.getHandCursor())
} else {
w.SetCursor(gui.getArrowCursor())
gui.window.SetCursor(gui.getArrowCursor())
}
}
func (gui *GUI) convertMouseCoordinates(px float64, py float64) (uint16, uint16) {
scale := gui.scale()
px = px / float64(scale)
py = py / float64(scale)
x := uint16(math.Floor((px - float64(gui.renderer.areaX)) / float64(gui.renderer.CellWidth())))
y := uint16(math.Floor((py - float64(gui.renderer.areaY)) / float64(gui.renderer.CellHeight())))
@ -122,7 +198,7 @@ func btnCode(button glfw.MouseButton, release bool, mod glfw.ModifierKey) (b byt
return b, true
}
func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey) {
func (gui *GUI) mouseButtonCallback(g *GUI, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey, mouseX float64, mouseY float64) {
if gui.overlay != nil {
if button == glfw.MouseButtonRight && action == glfw.Release {
@ -132,7 +208,7 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
}
// before we forward clicks on (below), we need to handle them locally for url clicking, text highlighting etc.
x, y := gui.convertMouseCoordinates(w.GetCursorPos())
x, y := gui.convertMouseCoordinates(mouseX, mouseY)
tx := int(x) + 1 // vt100 is 1 indexed
ty := int(y) + 1
@ -252,7 +328,10 @@ func (gui *GUI) mouseButtonCallback(w *glfw.Window, button glfw.MouseButton, act
default:
panic("Unsupported mouse mode")
}
}
func (gui *GUI) cursorEnterCallback(g *GUI, entered bool) {
// empty, just to conform to the mouseEventsHandler interface
}
func (gui *GUI) handleSelectionButtonPress(x uint16, y uint16, mod glfw.ModifierKey) {

157
gui/rectangleRenderer.go Normal file
View File

@ -0,0 +1,157 @@
package gui
import (
"github.com/go-gl/gl/all-core/gl"
"github.com/liamg/aminal/config"
)
const (
rectangleRendererVertexShaderSource = `
#version 330 core
layout (location = 0) in vec2 position;
uniform vec2 resolution;
void main() {
// convert from window coordinates to GL coordinates
vec2 glCoordinates = ((position / resolution) * 2.0 - 1.0) * vec2(1, -1);
gl_Position = vec4(glCoordinates, 0.0, 1.0);
}` + "\x00"
rectangleRendererFragmentShaderSource = `
#version 330 core
uniform vec4 inColor;
out vec4 outColor;
void main() {
outColor = inColor;
}` + "\x00"
)
type rectangleRenderer struct {
program uint32
vbo uint32
vao uint32
ibo uint32
uniformLocationResolution int32
uniformLocationInColor int32
}
func createRectangleRendererProgram() (uint32, error) {
vertexShader, err := compileShader(rectangleRendererVertexShaderSource, gl.VERTEX_SHADER)
if err != nil {
return 0, err
}
defer gl.DeleteShader(vertexShader)
fragmentShader, err := compileShader(rectangleRendererFragmentShaderSource, gl.FRAGMENT_SHADER)
if err != nil {
return 0, err
}
defer gl.DeleteShader(fragmentShader)
prog := gl.CreateProgram()
gl.AttachShader(prog, vertexShader)
gl.AttachShader(prog, fragmentShader)
gl.LinkProgram(prog)
return prog, nil
}
func newRectangleRenderer() (*rectangleRenderer, error) {
prog, err := createRectangleRendererProgram()
if err != nil {
return nil, err
}
var vbo uint32
var vao uint32
var ibo uint32
gl.GenBuffers(1, &vbo)
gl.GenVertexArrays(1, &vao)
gl.GenBuffers(1, &ibo)
indices := [...]uint32{
0, 1, 2,
2, 3, 0,
}
gl.BindVertexArray(vao)
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
gl.BufferData(gl.ARRAY_BUFFER, 8*4, nil, gl.DYNAMIC_DRAW) // just reserve data for the buffer
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo)
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(indices)*4, gl.Ptr(&indices[0]), gl.DYNAMIC_DRAW)
gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil)
gl.EnableVertexAttribArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
gl.BindVertexArray(0)
return &rectangleRenderer{
program: prog,
vbo: vbo,
vao: vao,
ibo: ibo,
uniformLocationResolution: gl.GetUniformLocation(prog, gl.Str("resolution\x00")),
uniformLocationInColor: gl.GetUniformLocation(prog, gl.Str("inColor\x00")),
}, nil
}
func (rr *rectangleRenderer) Free() {
if rr.program != 0 {
gl.DeleteProgram(rr.program)
rr.program = 0
}
if rr.vbo != 0 {
gl.DeleteBuffers(1, &rr.vbo)
rr.vbo = 0
}
if rr.vao != 0 {
gl.DeleteBuffers(1, &rr.vao)
rr.vao = 0
}
if rr.ibo != 0 {
gl.DeleteBuffers(1, &rr.ibo)
rr.ibo = 0
}
}
func (rr *rectangleRenderer) render(left float32, top float32, width float32, height float32, colour config.Colour) {
var savedProgram int32
gl.GetIntegerv(gl.CURRENT_PROGRAM, &savedProgram)
defer gl.UseProgram(uint32(savedProgram))
currentViewport := [4]int32{}
gl.GetIntegerv(gl.VIEWPORT, &currentViewport[0])
gl.UseProgram(rr.program)
gl.Uniform2f(rr.uniformLocationResolution, float32(currentViewport[2]), float32(currentViewport[3]))
gl.Uniform4f(rr.uniformLocationInColor, colour[0], colour[1], colour[2], 1.0)
vertices := [...]float32{
left, top,
left + width, top,
left + width, top + height,
left, top + height,
}
/*
gl.NamedBufferSubData(rr.vbo, 0, len(vertices)*4, gl.Ptr(&vertices[0]))
/*/
gl.BindBuffer(gl.ARRAY_BUFFER, rr.vbo)
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(vertices)*4, gl.Ptr(&vertices[0]))
//*/
gl.BindVertexArray(rr.vao)
gl.DrawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, gl.PtrOffset(0))
gl.BindVertexArray(0)
}

View File

@ -1,13 +1,12 @@
package gui
import (
"image"
"math"
"github.com/go-gl/gl/all-core/gl"
"github.com/liamg/aminal/buffer"
"github.com/liamg/aminal/config"
"github.com/liamg/aminal/glfont"
"image"
"math"
)
type OpenGLRenderer struct {
@ -26,16 +25,8 @@ type OpenGLRenderer struct {
textureMap map[*image.RGBA]uint32
fontMap *FontMap
backgroundColour [3]float32
}
type rectangle struct {
vao uint32
vbo uint32
cv uint32
colourAttr uint32
colour [3]float32
points [18]float32
prog uint32
rectRenderer *rectangleRenderer
}
type line struct {
@ -56,94 +47,6 @@ func (r *OpenGLRenderer) CellHeight() float32 {
return r.cellHeight
}
func (r *OpenGLRenderer) newRectangleEx(x float32, y float32, width float32, height float32, colourAttr uint32) *rectangle {
rect := &rectangle{}
halfAreaWidth := float32(r.areaWidth / 2)
halfAreaHeight := float32(r.areaHeight / 2)
x = (x - halfAreaWidth) / halfAreaWidth
y = -(y - (halfAreaHeight)) / halfAreaHeight
w := width / halfAreaWidth
h := height / halfAreaHeight
rect.points = [18]float32{
x, y, 0,
x, y + h, 0,
x + w, y + h, 0,
x + w, y, 0,
x, y, 0,
x + w, y + h, 0,
}
rect.colourAttr = colourAttr
rect.prog = r.program
// SHAPE
gl.GenBuffers(1, &rect.vbo)
gl.BindBuffer(gl.ARRAY_BUFFER, rect.vbo)
gl.BufferData(gl.ARRAY_BUFFER, 4*len(rect.points), gl.Ptr(&rect.points[0]), gl.STATIC_DRAW)
gl.GenVertexArrays(1, &rect.vao)
gl.BindVertexArray(rect.vao)
gl.EnableVertexAttribArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, rect.vbo)
gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)
// colour
gl.GenBuffers(1, &rect.cv)
rect.setColour([3]float32{0, 1, 0})
return rect
}
func (r *OpenGLRenderer) newRectangle(x float32, y float32, colourAttr uint32) *rectangle {
return r.newRectangleEx(x, y, r.cellWidth, r.cellHeight, colourAttr)
}
func (rect *rectangle) Draw() {
gl.UseProgram(rect.prog)
gl.BindVertexArray(rect.vao)
gl.DrawArrays(gl.TRIANGLES, 0, 6)
}
func (rect *rectangle) setColour(colour [3]float32) {
if rect.colour == colour {
return
}
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
}
func (rect *rectangle) Free() {
gl.DeleteVertexArrays(1, &rect.vao)
gl.DeleteBuffers(1, &rect.vbo)
gl.DeleteBuffers(1, &rect.cv)
rect.vao = 0
rect.vbo = 0
rect.cv = 0
}
func (r *OpenGLRenderer) newLine(x1 float32, y1 float32, x2 float32, y2 float32, dash float32, colourAttr uint32) *line {
l := &line{}
@ -249,7 +152,11 @@ func (l *line) Free() {
l.cv = 0
}
func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) *OpenGLRenderer {
func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) (*OpenGLRenderer, error) {
rectRenderer, err := newRectangleRenderer()
if err != nil {
return nil, err
}
r := &OpenGLRenderer{
areaWidth: areaWidth,
areaHeight: areaHeight,
@ -261,9 +168,10 @@ func NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY
program: program,
textureMap: map[*image.RGBA]uint32{},
fontMap: fontMap,
rectRenderer: rectRenderer,
}
r.SetArea(areaX, areaY, areaWidth, areaHeight)
return r
return r, nil
}
// This method ensures that all OpenGL resources are deleted correctly
@ -277,6 +185,11 @@ func (r *OpenGLRenderer) Free() {
gl.DeleteProgram(r.program)
r.program = 0
if r.rectRenderer != nil {
r.rectRenderer.Free()
r.rectRenderer = nil
}
}
func (r *OpenGLRenderer) GetTermSize() (uint, uint) {
@ -296,26 +209,16 @@ func (r *OpenGLRenderer) SetArea(areaX int, areaY int, areaWidth int, areaHeight
r.termRows = uint(math.Floor(float64(float32(r.areaHeight) / r.cellHeight)))
}
func (r *OpenGLRenderer) GetRectangleSize(col uint, row uint) (float32, float32) {
x := float32(float32(col) * r.cellWidth)
y := float32(float32(row) * r.cellHeight)
func (r *OpenGLRenderer) ConvertCoordinates(col uint, row uint) (float32, float32) {
left := float32(float32(col) * r.cellWidth)
top := float32(float32(row) * r.cellHeight)
return x, y
}
func (r *OpenGLRenderer) getRectangle(col uint, row uint) *rectangle {
x := float32(float32(col) * r.cellWidth)
y := float32(float32(row)*r.cellHeight) + r.cellHeight
return r.newRectangle(x, y, r.colourAttr)
return left, top
}
func (r *OpenGLRenderer) DrawCursor(col uint, row uint, colour config.Colour) {
rect := r.getRectangle(col, row)
rect.setColour(colour)
rect.Draw()
rect.Free()
left, top := r.ConvertCoordinates(col, row)
r.rectRenderer.render(left, top, r.cellWidth, r.cellHeight, colour)
}
func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, colour *config.Colour, force bool) {
@ -329,13 +232,9 @@ func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, colour
}
if bg != r.backgroundColour || force {
rect := r.getRectangle(col, row)
rect.setColour(bg)
rect.Draw()
rect.Free()
left, top := r.ConvertCoordinates(col, row)
r.rectRenderer.render(left, top, r.cellWidth, r.cellHeight, bg)
}
}
func (r *OpenGLRenderer) getUndelineThickness() float32 {
@ -353,12 +252,7 @@ func (r *OpenGLRenderer) DrawUnderline(span int, col uint, row uint, colour [3]f
y := (float32(row+1))*r.cellHeight + r.fontMap.DefaultFont().MinY()*0.25
thickness := r.getUndelineThickness()
rect := r.newRectangleEx(x, y, r.cellWidth*float32(span), thickness, r.colourAttr)
rect.setColour(colour)
rect.Draw()
rect.Free()
r.rectRenderer.render(x, y, r.cellWidth*float32(span), thickness, colour)
}
func (r *OpenGLRenderer) DrawLinkLine(span int, col uint, row uint, colour [3]float32) {

519
gui/scrollbar.go Normal file
View File

@ -0,0 +1,519 @@
package gui
import (
"github.com/go-gl/gl/all-core/gl"
"github.com/go-gl/glfw/v3.2/glfw"
)
const (
scrollbarVertexShaderSource = `
#version 330 core
layout (location = 0) in vec2 position;
uniform vec2 resolution;
void main() {
// convert from window coordinates to GL coordinates
vec2 glCoordinates = ((position / resolution) * 2.0 - 1.0) * vec2(1, -1);
gl_Position = vec4(glCoordinates, 0.0, 1.0);
}` + "\x00"
scrollbarFragmentShaderSource = `
#version 330 core
uniform vec4 inColor;
out vec4 outColor;
void main() {
outColor = inColor;
}` + "\x00"
NumberOfVertexValues = 100
)
var (
scrollbarColor_Bg = [3]float32{float32(241) / float32(255), float32(241) / float32(255), float32(241) / float32(255)}
scrollbarColor_ThumbNormal = [3]float32{float32(193) / float32(255), float32(193) / float32(255), float32(193) / float32(255)}
scrollbarColor_ThumbHover = [3]float32{float32(168) / float32(255), float32(168) / float32(255), float32(168) / float32(255)}
scrollbarColor_ThumbClicked = [3]float32{float32(120) / float32(255), float32(120) / float32(255), float32(120) / float32(255)}
scrollbarColor_ButtonNormalBg = [3]float32{float32(241) / float32(255), float32(241) / float32(255), float32(241) / float32(255)}
scrollbarColor_ButtonNormalFg = [3]float32{float32(80) / float32(255), float32(80) / float32(255), float32(80) / float32(255)}
scrollbarColor_ButtonHoverBg = [3]float32{float32(210) / float32(255), float32(210) / float32(255), float32(210) / float32(255)}
scrollbarColor_ButtonHoverFg = [3]float32{float32(80) / float32(255), float32(80) / float32(255), float32(80) / float32(255)}
scrollbarColor_ButtonDisabledBg = [3]float32{float32(241) / float32(255), float32(241) / float32(255), float32(241) / float32(255)}
scrollbarColor_ButtonDisabledFg = [3]float32{float32(163) / float32(255), float32(163) / float32(255), float32(163) / float32(255)}
scrollbarColor_ButtonClickedBg = [3]float32{float32(120) / float32(255), float32(120) / float32(255), float32(120) / float32(255)}
scrollbarColor_ButtonClickedFg = [3]float32{float32(255) / float32(255), float32(255) / float32(255), float32(255) / float32(255)}
)
type scrollbarPart int
const (
None scrollbarPart = iota
UpperArrow
UpperSpace // the space between upper arrow and thumb
Thumb
BottomSpace // the space between thumb and bottom arrow
BottomArrow
)
type ScreenRectangle struct {
left, top float32 // upper left corner in pixels relative to the window (in pixels)
right, bottom float32
}
func (sr *ScreenRectangle) width() float32 {
return sr.right - sr.left
}
func (sr *ScreenRectangle) height() float32 {
return sr.bottom - sr.top
}
func (sr *ScreenRectangle) isInside(x float32, y float32) bool {
return x >= sr.left && x < sr.right &&
y >= sr.top && y < sr.bottom
}
type scrollbar struct {
program uint32
vbo uint32
vao uint32
uniformLocationResolution int32
uniformLocationInColor int32
position ScreenRectangle // relative to the window's top left corner, in pixels
positionUpperArrow ScreenRectangle // relative to the control's top left corner
positionBottomArrow ScreenRectangle
positionThumb ScreenRectangle
scrollPosition int
maxScrollPosition int
thumbIsDragging bool
startedDraggingAtPosition int // scrollPosition when the dragging was started
startedDraggingAtThumbTop float32 // sb.positionThumb.top when the dragging was started
offsetInThumbY float32 // y offset inside the thumb of the dragging point
scrollPositionDelta int
upperArrowIsDown bool
bottomArrowIsDown bool
upperArrowFg []float32
upperArrowBg []float32
bottomArrowFg []float32
bottomArrowBg []float32
thumbColor []float32
}
// Returns the vertical scrollbar width in pixels
func getDefaultScrollbarWidth() int {
return 13
}
func createScrollbarProgram() (uint32, error) {
vertexShader, err := compileShader(scrollbarVertexShaderSource, gl.VERTEX_SHADER)
if err != nil {
return 0, err
}
defer gl.DeleteShader(vertexShader)
fragmentShader, err := compileShader(scrollbarFragmentShaderSource, gl.FRAGMENT_SHADER)
if err != nil {
return 0, err
}
defer gl.DeleteShader(fragmentShader)
prog := gl.CreateProgram()
gl.AttachShader(prog, vertexShader)
gl.AttachShader(prog, fragmentShader)
gl.LinkProgram(prog)
return prog, nil
}
func newScrollbar() (*scrollbar, error) {
prog, err := createScrollbarProgram()
if err != nil {
return nil, err
}
var vbo uint32
var vao uint32
gl.GenBuffers(1, &vbo)
gl.GenVertexArrays(1, &vao)
gl.BindVertexArray(vao)
gl.BindBuffer(gl.ARRAY_BUFFER, vbo)
gl.BufferData(gl.ARRAY_BUFFER, NumberOfVertexValues*4, nil, gl.DYNAMIC_DRAW) // only reserve the space
gl.VertexAttribPointer(0, 2, gl.FLOAT, false, 2*4, nil)
gl.EnableVertexAttribArray(0)
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
gl.BindVertexArray(0)
result := &scrollbar{
program: prog,
vbo: vbo,
vao: vao,
uniformLocationResolution: gl.GetUniformLocation(prog, gl.Str("resolution\x00")),
uniformLocationInColor: gl.GetUniformLocation(prog, gl.Str("inColor\x00")),
position: ScreenRectangle{
right: 0,
bottom: 0,
left: 0,
top: 0,
},
scrollPosition: 0,
maxScrollPosition: 0,
thumbIsDragging: false,
upperArrowIsDown: false,
bottomArrowIsDown: false,
}
result.recalcElementPositions()
result.resetElementColors(-1, -1) // (-1, -1) ensures that no part is hovered by the mouse
return result, nil
}
func (sb *scrollbar) Free() {
if sb.program != 0 {
gl.DeleteProgram(sb.program)
sb.program = 0
}
if sb.vbo != 0 {
gl.DeleteBuffers(1, &sb.vbo)
sb.vbo = 0
}
if sb.vao != 0 {
gl.DeleteBuffers(1, &sb.vao)
sb.vao = 0
}
}
// Recalc positions of the scrollbar elements according to current
func (sb *scrollbar) recalcElementPositions() {
arrowHeight := sb.position.width()
sb.positionUpperArrow = ScreenRectangle{
left: 0,
top: 0,
right: sb.position.width(),
bottom: arrowHeight,
}
sb.positionBottomArrow = ScreenRectangle{
left: sb.positionUpperArrow.left,
top: sb.position.height() - arrowHeight,
right: sb.positionUpperArrow.right,
bottom: sb.position.height(),
}
thumbHeight := sb.position.width()
thumbTop := arrowHeight
if sb.maxScrollPosition != 0 {
thumbTop += (float32(sb.scrollPosition) * (sb.position.height() - thumbHeight - arrowHeight*2)) / float32(sb.maxScrollPosition)
}
sb.positionThumb = ScreenRectangle{
left: 2,
top: thumbTop,
right: sb.position.width() - 2,
bottom: thumbTop + thumbHeight,
}
}
func (sb *scrollbar) resize(gui *GUI) {
sb.position.left = float32(gui.width) - float32(getDefaultScrollbarWidth())*gui.dpiScale
sb.position.top = float32(0.0)
sb.position.right = float32(gui.width)
sb.position.bottom = float32(gui.height - 1)
sb.recalcElementPositions()
gui.terminal.NotifyDirty()
}
func (sb *scrollbar) render(gui *GUI) {
var savedProgram int32
gl.GetIntegerv(gl.CURRENT_PROGRAM, &savedProgram)
defer gl.UseProgram(uint32(savedProgram))
gl.UseProgram(sb.program)
gl.Uniform2f(sb.uniformLocationResolution, float32(gui.width), float32(gui.height))
gl.BindVertexArray(sb.vao)
gl.BindBuffer(gl.ARRAY_BUFFER, sb.vbo)
defer func() {
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
gl.BindVertexArray(0)
}()
// Draw background
gl.Uniform4f(sb.uniformLocationInColor, scrollbarColor_Bg[0], scrollbarColor_Bg[1], scrollbarColor_Bg[2], 1.0)
borderVertices := [...]float32{
sb.position.left, sb.position.top,
sb.position.right, sb.position.top,
sb.position.right, sb.position.bottom,
sb.position.right, sb.position.bottom,
sb.position.left, sb.position.bottom,
sb.position.left, sb.position.top,
}
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(borderVertices)*4, gl.Ptr(&borderVertices[0]))
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(borderVertices)/2))
// Draw upper arrow
// Upper arrow background
gl.Uniform4f(sb.uniformLocationInColor, sb.upperArrowBg[0], sb.upperArrowBg[1], sb.upperArrowBg[2], 1.0)
upperArrowBgVertices := [...]float32{
sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.top,
sb.position.left + sb.positionUpperArrow.right, sb.position.top + sb.positionUpperArrow.top,
sb.position.left + sb.positionUpperArrow.right, sb.position.top + sb.positionUpperArrow.bottom,
sb.position.left + sb.positionUpperArrow.right, sb.position.top + sb.positionUpperArrow.bottom,
sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.bottom,
sb.position.left + sb.positionUpperArrow.left, sb.position.top + sb.positionUpperArrow.top,
}
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(upperArrowBgVertices)*4, gl.Ptr(&upperArrowBgVertices[0]))
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowBgVertices)/2))
// Upper arrow foreground
gl.Uniform4f(sb.uniformLocationInColor, sb.upperArrowFg[0], sb.upperArrowFg[1], sb.upperArrowFg[2], 1.0)
upperArrowFgVertices := [...]float32{
sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()/2.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/3.0,
sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()*2.0/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0,
sb.position.left + sb.positionUpperArrow.left + sb.positionUpperArrow.width()/3.0, sb.position.top + sb.positionUpperArrow.top + sb.positionUpperArrow.height()/2.0,
}
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(upperArrowFgVertices)*4, gl.Ptr(&upperArrowFgVertices[0]))
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(upperArrowFgVertices)/2))
// Draw bottom arrow
// Bottom arrow background
gl.Uniform4f(sb.uniformLocationInColor, sb.bottomArrowBg[0], sb.bottomArrowBg[1], sb.bottomArrowBg[2], 1.0)
bottomArrowBgVertices := [...]float32{
sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.top,
sb.position.left + sb.positionBottomArrow.right, sb.position.top + sb.positionBottomArrow.top,
sb.position.left + sb.positionBottomArrow.right, sb.position.top + sb.positionBottomArrow.bottom,
sb.position.left + sb.positionBottomArrow.right, sb.position.top + sb.positionBottomArrow.bottom,
sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.bottom,
sb.position.left + sb.positionBottomArrow.left, sb.position.top + sb.positionBottomArrow.top,
}
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(bottomArrowBgVertices)*4, gl.Ptr(&bottomArrowBgVertices[0]))
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowBgVertices)/2))
// Bottom arrow foreground
gl.Uniform4f(sb.uniformLocationInColor, sb.bottomArrowFg[0], sb.bottomArrowFg[1], sb.bottomArrowFg[2], 1.0)
bottomArrowFgVertices := [...]float32{
sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()/3.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()/2.0,
sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()*2.0/3.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()/2.0,
sb.position.left + sb.positionBottomArrow.left + sb.positionBottomArrow.width()/2.0, sb.position.top + sb.positionBottomArrow.top + sb.positionBottomArrow.height()*2.0/3.0,
}
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(bottomArrowFgVertices)*4, gl.Ptr(&bottomArrowFgVertices[0]))
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(bottomArrowFgVertices)/2))
// Draw thumb
gl.Uniform4f(sb.uniformLocationInColor, sb.thumbColor[0], sb.thumbColor[1], sb.thumbColor[2], 1.0)
thumbVertices := [...]float32{
sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.top,
sb.position.left + sb.positionThumb.right, sb.position.top + sb.positionThumb.top,
sb.position.left + sb.positionThumb.right, sb.position.top + sb.positionThumb.bottom,
sb.position.left + sb.positionThumb.right, sb.position.top + sb.positionThumb.bottom,
sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.bottom,
sb.position.left + sb.positionThumb.left, sb.position.top + sb.positionThumb.top,
}
gl.BufferSubData(gl.ARRAY_BUFFER, 0, len(thumbVertices)*4, gl.Ptr(&thumbVertices[0]))
gl.DrawArrays(gl.TRIANGLES, 0, int32(len(thumbVertices)/2))
gui.terminal.NotifyDirty()
}
func (sb *scrollbar) setPosition(max int, position int) {
if max <= 0 {
max = position
}
if position > max {
position = max
}
sb.maxScrollPosition = max
sb.scrollPosition = position
sb.recalcElementPositions()
}
func (sb *scrollbar) mouseHitTest(px float64, py float64) scrollbarPart {
// convert to local coordinates
mouseX := float32(px - float64(sb.position.left))
mouseY := float32(py - float64(sb.position.top))
result := None
if sb.positionUpperArrow.isInside(mouseX, mouseY) {
result = UpperArrow
} else if sb.positionBottomArrow.isInside(mouseX, mouseY) {
result = BottomArrow
} else if sb.positionThumb.isInside(mouseX, mouseY) {
result = Thumb
} else {
// construct UpperSpace
pos := ScreenRectangle{
left: sb.positionThumb.left,
top: sb.positionUpperArrow.bottom,
right: sb.positionThumb.right,
bottom: sb.positionThumb.top,
}
if pos.isInside(mouseX, mouseY) {
result = UpperSpace
}
// now update it to be BottomSpace
pos.top = sb.positionThumb.bottom
pos.bottom = sb.positionBottomArrow.top
if pos.isInside(mouseX, mouseY) {
result = BottomSpace
}
}
return result
}
func (sb *scrollbar) isMouseInside(px float64, py float64) bool {
return sb.position.isInside(float32(px), float32(py))
}
func (sb *scrollbar) mouseButtonCallback(g *GUI, button glfw.MouseButton, action glfw.Action, mod glfw.ModifierKey, mouseX float64, mouseY float64) {
if button == glfw.MouseButtonLeft {
if action == glfw.Press {
switch sb.mouseHitTest(mouseX, mouseY) {
case UpperArrow:
sb.upperArrowIsDown = true
g.terminal.ScreenScrollUp(1)
case UpperSpace:
g.terminal.ScrollPageUp()
case Thumb:
sb.thumbIsDragging = true
sb.startedDraggingAtPosition = sb.scrollPosition
sb.startedDraggingAtThumbTop = sb.positionThumb.top
sb.offsetInThumbY = float32(mouseY) - sb.position.top - sb.positionThumb.top
sb.scrollPositionDelta = 0
case BottomSpace:
g.terminal.ScrollPageDown()
case BottomArrow:
sb.bottomArrowIsDown = true
g.terminal.ScreenScrollDown(1)
}
} else if action == glfw.Release {
if sb.thumbIsDragging {
sb.thumbIsDragging = false
}
if sb.upperArrowIsDown {
sb.upperArrowIsDown = false
}
if sb.bottomArrowIsDown {
sb.bottomArrowIsDown = false
}
}
g.terminal.NotifyDirty()
}
sb.resetElementColors(mouseX, mouseY)
}
func (sb *scrollbar) mouseMoveCallback(g *GUI, px float64, py float64) {
sb.resetElementColors(px, py)
if sb.thumbIsDragging {
py -= float64(sb.position.top)
minThumbTop := sb.positionUpperArrow.bottom
maxThumbTop := sb.positionBottomArrow.top - sb.positionThumb.height()
newThumbTop := float32(py) - sb.offsetInThumbY
newPositionDelta := int((float32(sb.maxScrollPosition) * (newThumbTop - minThumbTop - sb.startedDraggingAtThumbTop)) / (maxThumbTop - minThumbTop))
if newPositionDelta > sb.scrollPositionDelta {
scrollLines := newPositionDelta - sb.scrollPositionDelta
g.logger.Debugf("old position: %d, new position delta: %d, scroll down %d lines", sb.scrollPosition, newPositionDelta, scrollLines)
g.terminal.ScreenScrollDown(uint16(scrollLines))
sb.scrollPositionDelta = newPositionDelta
} else if newPositionDelta < sb.scrollPositionDelta {
scrollLines := sb.scrollPositionDelta - newPositionDelta
g.logger.Debugf("old position: %d, new position delta: %d, scroll up %d lines", sb.scrollPosition, newPositionDelta, scrollLines)
g.terminal.ScreenScrollUp(uint16(scrollLines))
sb.scrollPositionDelta = newPositionDelta
}
sb.recalcElementPositions()
g.logger.Debugf("new thumbTop: %f, fact thumbTop: %f, position: %d", newThumbTop, sb.positionThumb.top, sb.scrollPosition)
}
g.terminal.NotifyDirty()
}
func (sb *scrollbar) resetElementColors(mouseX float64, mouseY float64) {
part := sb.mouseHitTest(mouseX, mouseY)
if sb.scrollPosition == 0 {
sb.upperArrowBg = scrollbarColor_ButtonDisabledBg[:]
sb.upperArrowFg = scrollbarColor_ButtonDisabledFg[:]
} else if sb.upperArrowIsDown {
sb.upperArrowFg = scrollbarColor_ButtonClickedFg[:]
sb.upperArrowBg = scrollbarColor_ButtonClickedBg[:]
} else if part == UpperArrow {
sb.upperArrowFg = scrollbarColor_ButtonHoverFg[:]
sb.upperArrowBg = scrollbarColor_ButtonHoverBg[:]
} else {
sb.upperArrowFg = scrollbarColor_ButtonNormalFg[:]
sb.upperArrowBg = scrollbarColor_ButtonNormalBg[:]
}
if sb.scrollPosition == sb.maxScrollPosition {
sb.bottomArrowBg = scrollbarColor_ButtonDisabledBg[:]
sb.bottomArrowFg = scrollbarColor_ButtonDisabledFg[:]
} else if sb.bottomArrowIsDown {
sb.bottomArrowFg = scrollbarColor_ButtonClickedFg[:]
sb.bottomArrowBg = scrollbarColor_ButtonClickedBg[:]
} else if part == BottomArrow {
sb.bottomArrowFg = scrollbarColor_ButtonHoverFg[:]
sb.bottomArrowBg = scrollbarColor_ButtonHoverBg[:]
} else {
sb.bottomArrowFg = scrollbarColor_ButtonNormalFg[:]
sb.bottomArrowBg = scrollbarColor_ButtonNormalBg[:]
}
if sb.thumbIsDragging {
sb.thumbColor = scrollbarColor_ThumbClicked[:]
} else if part == Thumb {
sb.thumbColor = scrollbarColor_ThumbHover[:]
} else {
sb.thumbColor = scrollbarColor_ThumbNormal[:]
}
}
func (sb *scrollbar) cursorEnterCallback(g *GUI, entered bool) {
if !entered {
sb.resetElementColors(-1, -1) // (-1, -1) ensures that no part is hovered by the mouse
g.terminal.NotifyDirty()
}
}

16
main.go
View File

@ -12,6 +12,7 @@ import (
"github.com/liamg/aminal/platform"
"github.com/liamg/aminal/terminal"
"github.com/riywo/loginshell"
"time"
)
type callback func(terminal *terminal.Terminal, g *gui.GUI)
@ -32,7 +33,17 @@ func initialize(unitTestfunc callback, configOverride *config.Config) {
fmt.Printf("Failed to create logger: %s\n", err)
os.Exit(1)
}
defer logger.Sync()
ticker := time.NewTicker(time.Second)
go func() {
for {
<-ticker.C
logger.Sync()
}
}()
defer func() {
ticker.Stop()
logger.Sync()
}()
if conf.CPUProfile != "" {
logger.Infof("Starting CPU profiling...")
@ -61,7 +72,7 @@ func initialize(unitTestfunc callback, configOverride *config.Config) {
guestProcess, err := pty.CreateGuestProcess(shellStr)
if err != nil {
pty.Close()
logger.Fatalf("Failed to start your shell: %s", err)
logger.Fatalf("Failed to start your shell %q: %s", shellStr, err)
}
defer guestProcess.Close()
@ -72,6 +83,7 @@ func initialize(unitTestfunc callback, configOverride *config.Config) {
if err != nil {
logger.Fatalf("Cannot start: %s", err)
}
defer g.Free()
terminal.WindowManipulation = g

View File

@ -11,11 +11,10 @@ import (
"testing"
"time"
"github.com/carlogit/phash"
"github.com/liamg/aminal/config"
"github.com/liamg/aminal/gui"
"github.com/liamg/aminal/terminal"
"github.com/carlogit/phash"
)
var termRef *terminal.Terminal
@ -47,10 +46,14 @@ func hash(path string) string {
return imageHash
}
func imagesAreEqual(expected string, actual string) int {
expectedHash := hash(expected)
actualHash := hash(actual)
return phash.GetDistance(expectedHash, actualHash)
}
func compareImages(expected string, actual string) {
template := hash(expected)
screen := hash(actual)
distance := phash.GetDistance(template, screen)
distance := imagesAreEqual(expected, actual)
if distance != 0 {
os.Exit(terminate(fmt.Sprintf("Screenshot \"%s\" doesn't match expected image \"%s\". Distance of hashes difference: %d\n",
actual, expected, distance)))
@ -58,19 +61,55 @@ func compareImages(expected string, actual string) {
}
func send(terminal *terminal.Terminal, cmd string) {
terminal.Write([]byte(cmd))
err := terminal.Write([]byte(cmd))
if err != nil {
panic(err)
}
}
func enter(terminal *terminal.Terminal) {
terminal.Write([]byte("\n"))
err := terminal.Write([]byte("\n"))
if err != nil {
panic(err)
}
}
func validateScreen(img string) {
guiRef.Screenshot(img)
func validateScreen(img string, waitForChange bool) {
fmt.Printf("taking screenshot: %s and comparing...", img)
err := guiRef.Screenshot(img)
if err != nil {
panic(err)
}
compareImages(strings.Join([]string{"vttest/", img}, ""), img)
fmt.Printf("compare OK\n")
enter(termRef)
sleep()
if waitForChange {
fmt.Print("Waiting for screen change...")
attempts := 10
for {
sleep()
actualScren := "temp.png"
err = guiRef.Screenshot(actualScren)
if err != nil {
panic(err)
}
distance := imagesAreEqual(actualScren, img)
if distance != 0 {
break
}
fmt.Printf(" %d", attempts)
attempts--
if attempts <= 0 {
break
}
}
fmt.Print("done\n")
}
}
func TestMain(m *testing.M) {
@ -115,12 +154,12 @@ func TestCursorMovement(t *testing.T) {
os.Exit(terminate(fmt.Sprintf("ActiveBuffer doesn't match vttest template vttest/test-cursor-movement-1")))
}
validateScreen("test-cursor-movement-1.png")
validateScreen("test-cursor-movement-2.png")
validateScreen("test-cursor-movement-3.png")
validateScreen("test-cursor-movement-4.png")
validateScreen("test-cursor-movement-5.png")
validateScreen("test-cursor-movement-6.png")
validateScreen("test-cursor-movement-1.png", true)
validateScreen("test-cursor-movement-2.png", true)
validateScreen("test-cursor-movement-3.png", true)
validateScreen("test-cursor-movement-4.png", true)
validateScreen("test-cursor-movement-5.png", true)
validateScreen("test-cursor-movement-6.png", false)
g.Close()
}
@ -142,21 +181,21 @@ func TestScreenFeatures(t *testing.T) {
send(term, "2\n")
sleep()
validateScreen("test-screen-features-1.png")
validateScreen("test-screen-features-2.png")
validateScreen("test-screen-features-3.png")
validateScreen("test-screen-features-4.png")
validateScreen("test-screen-features-5.png")
validateScreen("test-screen-features-6.png")
validateScreen("test-screen-features-7.png")
validateScreen("test-screen-features-8.png")
validateScreen("test-screen-features-9.png")
validateScreen("test-screen-features-10.png")
validateScreen("test-screen-features-11.png")
validateScreen("test-screen-features-12.png")
validateScreen("test-screen-features-13.png")
validateScreen("test-screen-features-14.png")
validateScreen("test-screen-features-15.png")
validateScreen("test-screen-features-1.png", true)
validateScreen("test-screen-features-2.png", true)
validateScreen("test-screen-features-3.png", true)
validateScreen("test-screen-features-4.png", true)
validateScreen("test-screen-features-5.png", true)
validateScreen("test-screen-features-6.png", true)
validateScreen("test-screen-features-7.png", true)
validateScreen("test-screen-features-8.png", true)
validateScreen("test-screen-features-9.png", true)
validateScreen("test-screen-features-10.png", true)
validateScreen("test-screen-features-11.png", true)
validateScreen("test-screen-features-12.png", true)
validateScreen("test-screen-features-13.png", true)
validateScreen("test-screen-features-14.png", true)
validateScreen("test-screen-features-15.png", false)
g.Close()
}
@ -178,10 +217,9 @@ func TestSixel(t *testing.T) {
send(term, "clear\n")
sleep()
send(term, "cat example.sixel\n")
sleep(4)
sleep(10) // Displaying SIXEL graphics *sometimes* takes long time. Excessive synchronization???
guiRef.Screenshot("test-sixel.png")
validateScreen("test-sixel.png")
validateScreen("test-sixel.png", false)
g.Close()
}
@ -198,6 +236,9 @@ func TestExit(t *testing.T) {
func testConfig() *config.Config {
c := config.DefaultConfig()
// Force the scrollbar not showing when running unit tests
c.ShowVerticalScrollbar = false
// Use a vanilla shell on POSIX to help ensure consistency.
if runtime.GOOS != "windows" {
c.Shell = "/bin/sh"

View File

@ -4,12 +4,8 @@ package platform
import (
"errors"
"syscall"
"time"
"fmt"
"github.com/MaxRis/w32"
"syscall"
)
// #include "windows.h"
@ -144,42 +140,7 @@ func (pty *winConPty) CreateGuestProcess(imagePath string) (Process, error) {
pty.processID = process.processID
err = setupChildConsole(C.DWORD(process.processID), C.STD_OUTPUT_HANDLE, C.ENABLE_PROCESSED_OUTPUT|C.ENABLE_WRAP_AT_EOL_OUTPUT)
if err != nil {
process.Close()
return nil, err
}
return process, err
}
func setupChildConsole(processID C.DWORD, nStdHandle C.DWORD, mode uint) error {
C.FreeConsole()
defer C.AttachConsole(^C.DWORD(0)) // attach to parent process console
// process may not be ready so we'll do retries
const maxWaitMilliSeconds = 5000
const waitStepMilliSeconds = 200
count := maxWaitMilliSeconds / waitStepMilliSeconds
for {
if r := C.AttachConsole(processID); r != 0 {
break // success
}
lastError := C.GetLastError()
if lastError != C.ERROR_GEN_FAILURE || count <= 0 {
return fmt.Errorf("Was not able to attach to the child prosess' console")
}
time.Sleep(time.Millisecond * time.Duration(waitStepMilliSeconds))
count--
}
h := C.GetStdHandle(nStdHandle)
C.SetConsoleMode(h, C.DWORD(mode))
C.FreeConsole()
return nil
return process, nil
}
func (pty *winConPty) Resize(x, y int) error {