From c2a7be2aeb726a706c600b649b3657bcd996c47a Mon Sep 17 00:00:00 2001 From: nikitar020 <42252263+nikitar020@users.noreply.github.com> Date: Tue, 19 Mar 2019 19:57:17 +0200 Subject: [PATCH] Implementation of the vertical scrollbar (#229) * Implementation of the vertical scrollbar (also enable gofmt checks only for go1.11.x builds) --- README.md | 33 +-- buffer/buffer.go | 15 +- config/config.go | 1 + config/defaults.go | 1 + gui/fonts.go | 10 +- gui/gui.go | 125 +++++++--- gui/mouse.go | 95 ++++++- gui/rectangleRenderer.go | 157 ++++++++++++ gui/renderer.go | 150 ++--------- gui/scrollbar.go | 519 +++++++++++++++++++++++++++++++++++++++ main.go | 1 + main_test.go | 3 + 12 files changed, 923 insertions(+), 187 deletions(-) create mode 100644 gui/rectangleRenderer.go create mode 100644 gui/scrollbar.go diff --git a/README.md b/README.md index bfe5e8a..26d14ff 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/buffer/buffer.go b/buffer/buffer.go index 201931a..7ec9f93 100644 --- a/buffer/buffer.go +++ b/buffer/buffer.go @@ -518,6 +518,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 @@ -545,7 +554,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 @@ -641,7 +650,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] @@ -1115,7 +1124,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) diff --git a/config/config.go b/config/config.go index 11add72..dff0785 100644 --- a/config/config.go +++ b/config/config.go @@ -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"` diff --git a/config/defaults.go b/config/defaults.go index 31eb279..6a62c78 100644 --- a/config/defaults.go +++ b/config/defaults.go @@ -39,6 +39,7 @@ func DefaultConfig() *Config { SearchURL: "https://www.google.com/search?q=$QUERY", MaxLines: 1000, CopyAndPasteWithMouse: true, + ShowVerticalScrollbar: true, } } diff --git a/gui/fonts.go b/gui/fonts.go index 3e0aaf6..911be0e 100644 --- a/gui/fonts.go +++ b/gui/fonts.go @@ -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 } diff --git a/gui/gui.go b/gui/gui.go index be228f5..7c6b9af 100644 --- a/gui/gui.go +++ b/gui/gui.go @@ -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!") @@ -338,14 +377,14 @@ 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 +394,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 +414,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 +458,10 @@ func (gui *GUI) Render() error { gui.Close() }() + if gui.vScrollbar != nil { + gui.vScrollbar.resize(gui) + } + gui.logger.Debugf("Starting render...") gl.UseProgram(program) @@ -677,8 +735,9 @@ func (gui *GUI) renderTerminalData(shouldLock bool) { gui.renderer.DrawUnderline(span, uint(x-span), uint(y), colour) } } - } + + gui.renderScrollbar() } func (gui *GUI) redraw(shouldLock bool) { @@ -832,6 +891,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 { diff --git a/gui/mouse.go b/gui/mouse.go index 14c71c5..19703e9 100644 --- a/gui/mouse.go +++ b/gui/mouse.go @@ -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) { diff --git a/gui/rectangleRenderer.go b/gui/rectangleRenderer.go new file mode 100644 index 0000000..484fe51 --- /dev/null +++ b/gui/rectangleRenderer.go @@ -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, ¤tViewport[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) +} diff --git a/gui/renderer.go b/gui/renderer.go index b5aee10..9b9e6ab 100644 --- a/gui/renderer.go +++ b/gui/renderer.go @@ -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 } func (r *OpenGLRenderer) CellWidth() float32 { @@ -46,95 +37,12 @@ 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, +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 } - 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 NewOpenGLRenderer(config *config.Config, fontMap *FontMap, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) *OpenGLRenderer { r := &OpenGLRenderer{ areaWidth: areaWidth, areaHeight: areaHeight, @@ -146,9 +54,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 @@ -162,6 +71,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) { @@ -181,26 +95,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) { @@ -214,13 +118,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) } - } // DrawUnderline draws a line under 'span' characters starting at (col, row) @@ -233,12 +133,8 @@ func (r *OpenGLRenderer) DrawUnderline(span int, col uint, row uint, colour [3]f if thickness < 1 { thickness = 1 } - 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) DrawCellText(text string, col uint, row uint, alpha float32, colour [3]float32, bold bool) { diff --git a/gui/scrollbar.go b/gui/scrollbar.go new file mode 100644 index 0000000..a8b5c54 --- /dev/null +++ b/gui/scrollbar.go @@ -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() + } +} diff --git a/main.go b/main.go index 1b4114c..b0b4eab 100644 --- a/main.go +++ b/main.go @@ -72,6 +72,7 @@ func initialize(unitTestfunc callback, configOverride *config.Config) { if err != nil { logger.Fatalf("Cannot start: %s", err) } + defer g.Free() terminal.WindowManipulation = g diff --git a/main_test.go b/main_test.go index ab91005..1e833c7 100644 --- a/main_test.go +++ b/main_test.go @@ -198,6 +198,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"