484 lines
11 KiB
Go
484 lines
11 KiB
Go
|
package main
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"runtime"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/go-gl/gl/v4.1-core/gl"
|
||
|
"github.com/go-gl/glfw/v3.3/glfw"
|
||
|
"github.com/go-gl/gltext"
|
||
|
"github.com/go-gl/mathgl/mgl32"
|
||
|
)
|
||
|
|
||
|
func init() {
|
||
|
// GLFW event handling must run on the main OS thread
|
||
|
runtime.LockOSThread()
|
||
|
}
|
||
|
|
||
|
// Position represents a 2D position
|
||
|
type Position struct {
|
||
|
X, Y float32
|
||
|
}
|
||
|
|
||
|
// Size represents the width and height of a UI element
|
||
|
type Size struct {
|
||
|
Width, Height float32
|
||
|
}
|
||
|
|
||
|
// EventType represents the type of an event
|
||
|
type EventType int
|
||
|
|
||
|
const (
|
||
|
EventTypeClick EventType = iota
|
||
|
EventTypeMouseMove
|
||
|
EventTypeHover
|
||
|
)
|
||
|
|
||
|
// Event represents an input event
|
||
|
type Event struct {
|
||
|
Type EventType
|
||
|
Position Position
|
||
|
}
|
||
|
|
||
|
// UIElement defines the interface for UI elements
|
||
|
type UIElement interface {
|
||
|
Draw()
|
||
|
Update()
|
||
|
HandleEvent(event Event)
|
||
|
}
|
||
|
|
||
|
// Container defines the interface for UI containers
|
||
|
type Container interface {
|
||
|
UIElement
|
||
|
AddChild(UIElement)
|
||
|
RemoveChild(UIElement)
|
||
|
}
|
||
|
|
||
|
// Context manages the OpenGL context and resources
|
||
|
type Context struct {
|
||
|
shaderProgram uint32
|
||
|
font *gltext.Font
|
||
|
windowWidth int
|
||
|
windowHeight int
|
||
|
}
|
||
|
|
||
|
func NewContext(windowWidth, windowHeight int) *Context {
|
||
|
return &Context{
|
||
|
windowWidth: windowWidth,
|
||
|
windowHeight: windowHeight,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (c *Context) InitOpenGL() {
|
||
|
if err := gl.Init(); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
version := gl.GoStr(gl.GetString(gl.VERSION))
|
||
|
fmt.Println("OpenGL version", version)
|
||
|
|
||
|
// Initialize shader program
|
||
|
c.shaderProgram = NewShaderProgram(vertexShaderSource, fragmentShaderSource)
|
||
|
|
||
|
// Load font using gltext
|
||
|
fontData, err := os.Open("arial.ttf")
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
defer fontData.Close()
|
||
|
|
||
|
// Load the truetype font: specify scale, rune range (ASCII), and direction
|
||
|
font, err := gltext.LoadTruetype(fontData, 24, 32, 127, gltext.LeftToRight)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
c.font = font
|
||
|
}
|
||
|
|
||
|
func (c *Context) Clear() {
|
||
|
gl.ClearColor(0.0, 0.0, 0.0, 1.0)
|
||
|
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||
|
}
|
||
|
|
||
|
func (c *Context) SwapBuffers(window *glfw.Window) {
|
||
|
window.SwapBuffers()
|
||
|
}
|
||
|
|
||
|
// Window represents the main application window
|
||
|
type Window struct {
|
||
|
width int
|
||
|
height int
|
||
|
title string
|
||
|
transparent bool
|
||
|
panels []*Panel
|
||
|
glContext *Context
|
||
|
glfwWindow *glfw.Window
|
||
|
}
|
||
|
|
||
|
func NewWindow(width, height int, title string) *Window {
|
||
|
if err := glfw.Init(); err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
glfw.WindowHint(glfw.ContextVersionMajor, 4)
|
||
|
glfw.WindowHint(glfw.ContextVersionMinor, 1)
|
||
|
glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile)
|
||
|
glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True)
|
||
|
|
||
|
glfwWindow, err := glfw.CreateWindow(width, height, title, nil, nil)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
glfwWindow.MakeContextCurrent()
|
||
|
|
||
|
return &Window{
|
||
|
width: width,
|
||
|
height: height,
|
||
|
title: title,
|
||
|
panels: []*Panel{},
|
||
|
glfwWindow: glfwWindow,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (w *Window) SetTransparent(t bool) {
|
||
|
w.transparent = t
|
||
|
// Apply transparency settings if needed
|
||
|
}
|
||
|
|
||
|
func (w *Window) DefineContext() *Context {
|
||
|
w.glContext = NewContext(w.width, w.height)
|
||
|
w.glContext.InitOpenGL()
|
||
|
return w.glContext
|
||
|
}
|
||
|
|
||
|
func (w *Window) CreatePanel() *Panel {
|
||
|
panel := NewPanel()
|
||
|
w.panels = append(w.panels, panel)
|
||
|
return panel
|
||
|
}
|
||
|
|
||
|
func (w *Window) registerCallbacks() {
|
||
|
w.glfwWindow.SetCursorPosCallback(w.cursorPosCallback)
|
||
|
w.glfwWindow.SetMouseButtonCallback(w.mouseButtonCallback)
|
||
|
}
|
||
|
|
||
|
func (w *Window) cursorPosCallback(window *glfw.Window, xpos float64, ypos float64) {
|
||
|
event := Event{
|
||
|
Type: EventTypeMouseMove,
|
||
|
Position: Position{X: float32(xpos), Y: float32(ypos)},
|
||
|
}
|
||
|
w.handleEvent(event)
|
||
|
}
|
||
|
|
||
|
func (w *Window) mouseButtonCallback(window *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
|
||
|
if button == glfw.MouseButtonLeft && action == glfw.Press {
|
||
|
xpos, ypos := window.GetCursorPos()
|
||
|
event := Event{
|
||
|
Type: EventTypeClick,
|
||
|
Position: Position{X: float32(xpos), Y: float32(ypos)},
|
||
|
}
|
||
|
w.handleEvent(event)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (w *Window) handleEvent(event Event) {
|
||
|
for _, panel := range w.panels {
|
||
|
panel.HandleEvent(event)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (w *Window) update() {
|
||
|
for _, panel := range w.panels {
|
||
|
panel.Update()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (w *Window) draw() {
|
||
|
for _, panel := range w.panels {
|
||
|
panel.Draw()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (w *Window) Start() {
|
||
|
w.registerCallbacks()
|
||
|
for !w.glfwWindow.ShouldClose() {
|
||
|
glfw.PollEvents()
|
||
|
w.glContext.Clear()
|
||
|
|
||
|
w.update()
|
||
|
w.draw()
|
||
|
|
||
|
w.glContext.SwapBuffers(w.glfwWindow)
|
||
|
}
|
||
|
glfw.Terminate()
|
||
|
}
|
||
|
|
||
|
// Panel is a container for UI elements
|
||
|
type Panel struct {
|
||
|
children []UIElement
|
||
|
}
|
||
|
|
||
|
func NewPanel() *Panel {
|
||
|
return &Panel{
|
||
|
children: []UIElement{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *Panel) AddChild(e UIElement) {
|
||
|
p.children = append(p.children, e)
|
||
|
}
|
||
|
|
||
|
func (p *Panel) RemoveChild(e UIElement) {
|
||
|
// Implement child removal logic if needed
|
||
|
}
|
||
|
|
||
|
func (p *Panel) Draw() {
|
||
|
for _, child := range p.children {
|
||
|
child.Draw()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *Panel) Update() {
|
||
|
for _, child := range p.children {
|
||
|
child.Update()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (p *Panel) HandleEvent(event Event) {
|
||
|
for _, child := range p.children {
|
||
|
child.HandleEvent(event)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Button is a clickable UI element
|
||
|
type Button struct {
|
||
|
label string
|
||
|
onClick func()
|
||
|
Position
|
||
|
Size
|
||
|
isHovered bool
|
||
|
context *Context
|
||
|
}
|
||
|
|
||
|
func NewButton(label string, onClick func(), context *Context) *Button {
|
||
|
return &Button{
|
||
|
label: label,
|
||
|
onClick: onClick,
|
||
|
context: context,
|
||
|
Position: Position{X: 0, Y: 0},
|
||
|
Size: Size{Width: 100, Height: 50},
|
||
|
isHovered: false,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (b *Button) contains(pos Position) bool {
|
||
|
return pos.X >= b.Position.X && pos.X <= b.Position.X+b.Size.Width &&
|
||
|
pos.Y >= b.Position.Y && pos.Y <= b.Position.Y+b.Size.Height
|
||
|
}
|
||
|
|
||
|
func (b *Button) Draw() {
|
||
|
var color [4]float32
|
||
|
if b.isHovered {
|
||
|
color = [4]float32{0.7, 0.7, 0.7, 1.0} // Lighter when hovered
|
||
|
} else {
|
||
|
color = [4]float32{0.5, 0.5, 0.5, 1.0}
|
||
|
}
|
||
|
|
||
|
rect := NewRectangle(b.Position.X, b.Position.Y, b.Size.Width, b.Size.Height, color, b.context)
|
||
|
rect.Draw()
|
||
|
rect.Destroy()
|
||
|
|
||
|
// Draw the label
|
||
|
x := b.Position.X + 10
|
||
|
y := b.Position.Y + b.Size.Height/2 + 8 // Approximate vertical centering
|
||
|
b.context.font.Printf(x, y, b.label)
|
||
|
}
|
||
|
|
||
|
func (b *Button) Update() {
|
||
|
// Update the button if needed
|
||
|
}
|
||
|
|
||
|
func (b *Button) HandleEvent(event Event) {
|
||
|
switch event.Type {
|
||
|
case EventTypeClick:
|
||
|
if b.contains(event.Position) {
|
||
|
b.onClick()
|
||
|
}
|
||
|
case EventTypeMouseMove:
|
||
|
if b.contains(event.Position) {
|
||
|
if !b.isHovered {
|
||
|
b.isHovered = true
|
||
|
}
|
||
|
} else {
|
||
|
if b.isHovered {
|
||
|
b.isHovered = false
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Rectangle is used to draw a rectangle using OpenGL
|
||
|
type Rectangle struct {
|
||
|
Position
|
||
|
Size
|
||
|
Color [4]float32
|
||
|
vao uint32
|
||
|
vbo uint32
|
||
|
shaderProgram uint32
|
||
|
context *Context
|
||
|
}
|
||
|
|
||
|
func NewRectangle(x, y, width, height float32, color [4]float32, context *Context) *Rectangle {
|
||
|
rect := &Rectangle{
|
||
|
Position: Position{X: x, Y: y},
|
||
|
Size: Size{Width: width, Height: height},
|
||
|
Color: color,
|
||
|
shaderProgram: context.shaderProgram,
|
||
|
context: context,
|
||
|
}
|
||
|
rect.initOpenGL()
|
||
|
return rect
|
||
|
}
|
||
|
|
||
|
func (r *Rectangle) initOpenGL() {
|
||
|
vertices := []float32{
|
||
|
r.Position.X, r.Position.Y, 0.0,
|
||
|
r.Position.X + r.Size.Width, r.Position.Y, 0.0,
|
||
|
r.Position.X + r.Size.Width, r.Position.Y + r.Size.Height, 0.0,
|
||
|
r.Position.X, r.Position.Y + r.Size.Height, 0.0,
|
||
|
}
|
||
|
indices := []uint32{
|
||
|
0, 1, 2,
|
||
|
2, 3, 0,
|
||
|
}
|
||
|
gl.GenVertexArrays(1, &r.vao)
|
||
|
gl.BindVertexArray(r.vao)
|
||
|
|
||
|
gl.GenBuffers(1, &r.vbo)
|
||
|
gl.BindBuffer(gl.ARRAY_BUFFER, r.vbo)
|
||
|
gl.BufferData(gl.ARRAY_BUFFER, len(vertices)*4, gl.Ptr(vertices), gl.STATIC_DRAW)
|
||
|
|
||
|
var ebo uint32
|
||
|
gl.GenBuffers(1, &ebo)
|
||
|
gl.BindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo)
|
||
|
gl.BufferData(gl.ELEMENT_ARRAY_BUFFER, len(indices)*4, gl.Ptr(indices), gl.STATIC_DRAW)
|
||
|
|
||
|
gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 3*4, gl.PtrOffset(0))
|
||
|
gl.EnableVertexAttribArray(0)
|
||
|
|
||
|
gl.BindVertexArray(0)
|
||
|
}
|
||
|
|
||
|
func (r *Rectangle) Draw() {
|
||
|
gl.UseProgram(r.shaderProgram)
|
||
|
|
||
|
// Set uniforms
|
||
|
model := mgl32.Ident4()
|
||
|
projection := mgl32.Ortho2D(0, float32(r.context.windowWidth), float32(r.context.windowHeight), 0)
|
||
|
|
||
|
modelLoc := gl.GetUniformLocation(r.shaderProgram, gl.Str("model\x00"))
|
||
|
projectionLoc := gl.GetUniformLocation(r.shaderProgram, gl.Str("projection\x00"))
|
||
|
colorLoc := gl.GetUniformLocation(r.shaderProgram, gl.Str("color\x00"))
|
||
|
|
||
|
gl.UniformMatrix4fv(modelLoc, 1, false, &model[0])
|
||
|
gl.UniformMatrix4fv(projectionLoc, 1, false, &projection[0])
|
||
|
gl.Uniform4fv(colorLoc, 1, &r.Color[0])
|
||
|
|
||
|
gl.BindVertexArray(r.vao)
|
||
|
gl.DrawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, gl.PtrOffset(0))
|
||
|
gl.BindVertexArray(0)
|
||
|
}
|
||
|
|
||
|
func (r *Rectangle) Destroy() {
|
||
|
gl.DeleteVertexArrays(1, &r.vao)
|
||
|
gl.DeleteBuffers(1, &r.vbo)
|
||
|
}
|
||
|
|
||
|
// Shader sources
|
||
|
var vertexShaderSource = `
|
||
|
#version 410 core
|
||
|
layout(location = 0) in vec3 position;
|
||
|
uniform mat4 model;
|
||
|
uniform mat4 projection;
|
||
|
void main() {
|
||
|
gl_Position = projection * model * vec4(position, 1.0);
|
||
|
}
|
||
|
` + "\x00"
|
||
|
|
||
|
var fragmentShaderSource = `
|
||
|
#version 410 core
|
||
|
out vec4 fragColor;
|
||
|
uniform vec4 color;
|
||
|
void main() {
|
||
|
fragColor = color;
|
||
|
}
|
||
|
` + "\x00"
|
||
|
|
||
|
// NewShaderProgram compiles and links the vertex and fragment shaders
|
||
|
func NewShaderProgram(vertexShaderSource, fragmentShaderSource string) uint32 {
|
||
|
vertexShader := gl.CreateShader(gl.VERTEX_SHADER)
|
||
|
cvertexShaderSource, freeVertex := gl.Strs(vertexShaderSource)
|
||
|
gl.ShaderSource(vertexShader, 1, cvertexShaderSource, nil)
|
||
|
freeVertex()
|
||
|
gl.CompileShader(vertexShader)
|
||
|
|
||
|
var status int32
|
||
|
gl.GetShaderiv(vertexShader, gl.COMPILE_STATUS, &status)
|
||
|
if status == gl.FALSE {
|
||
|
var logLength int32
|
||
|
gl.GetShaderiv(vertexShader, gl.INFO_LOG_LENGTH, &logLength)
|
||
|
log := strings.Repeat("\x00", int(logLength+1))
|
||
|
gl.GetShaderInfoLog(vertexShader, logLength, nil, gl.Str(log))
|
||
|
panic(fmt.Sprintf("failed to compile vertex shader: %v", log))
|
||
|
}
|
||
|
|
||
|
fragmentShader := gl.CreateShader(gl.FRAGMENT_SHADER)
|
||
|
cfragmentShaderSource, freeFragment := gl.Strs(fragmentShaderSource)
|
||
|
gl.ShaderSource(fragmentShader, 1, cfragmentShaderSource, nil)
|
||
|
freeFragment()
|
||
|
gl.CompileShader(fragmentShader)
|
||
|
|
||
|
gl.GetShaderiv(fragmentShader, gl.COMPILE_STATUS, &status)
|
||
|
if status == gl.FALSE {
|
||
|
var logLength int32
|
||
|
gl.GetShaderiv(fragmentShader, gl.INFO_LOG_LENGTH, &logLength)
|
||
|
log := strings.Repeat("\x00", int(logLength+1))
|
||
|
gl.GetShaderInfoLog(fragmentShader, logLength, nil, gl.Str(log))
|
||
|
panic(fmt.Sprintf("failed to compile fragment shader: %v", log))
|
||
|
}
|
||
|
|
||
|
program := gl.CreateProgram()
|
||
|
gl.AttachShader(program, vertexShader)
|
||
|
gl.AttachShader(program, fragmentShader)
|
||
|
gl.LinkProgram(program)
|
||
|
|
||
|
gl.GetProgramiv(program, gl.LINK_STATUS, &status)
|
||
|
if status == gl.FALSE {
|
||
|
var logLength int32
|
||
|
gl.GetProgramiv(program, gl.INFO_LOG_LENGTH, &logLength)
|
||
|
log := strings.Repeat("\x00", int(logLength+1))
|
||
|
gl.GetProgramInfoLog(program, logLength, nil, gl.Str(log))
|
||
|
panic(fmt.Sprintf("failed to link program: %v", log))
|
||
|
}
|
||
|
|
||
|
gl.DeleteShader(vertexShader)
|
||
|
gl.DeleteShader(fragmentShader)
|
||
|
|
||
|
return program
|
||
|
}
|
||
|
|
||
|
func main() {
|
||
|
window := NewWindow(800, 600, "Demo Window")
|
||
|
window.SetTransparent(true)
|
||
|
context := window.DefineContext()
|
||
|
|
||
|
panel := window.CreatePanel()
|
||
|
|
||
|
button := NewButton("Click me!", func() { fmt.Println("Button pressed!") }, context)
|
||
|
button.Position = Position{X: 100, Y: 100}
|
||
|
button.Size = Size{Width: 200, Height: 50}
|
||
|
panel.AddChild(button)
|
||
|
|
||
|
window.Start()
|
||
|
}
|