aminal/gui/scrollbar.go

520 lines
18 KiB
Go

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()
}
}