Initial commit: Add Go module, basic window management, and widget framework with fill and layout capabilities.

This commit is contained in:
2025-10-26 20:13:21 +00:00
parent 0fd5445ec6
commit 16502b8e19
8 changed files with 1193 additions and 0 deletions

180
cmd/hello/main.go Normal file
View File

@@ -0,0 +1,180 @@
package main
import (
"github.com/go-gl/gl/all-core/gl"
"github.com/mleku/goo/pkg/interfaces"
"github.com/mleku/goo/pkg/widget"
"github.com/mleku/goo/pkg/window"
"lol.mleku.dev/chk"
)
// WidgetApp implements the window application
type WidgetApp struct {
rootWidget *widget.RootWidget
}
// Init initializes the widget tree
func (app *WidgetApp) Init() (err error) {
// Create four fill widgets with different colors
redFill := widget.NewFlexFill(
1.0, 0.0, 0.0, 1.0, // Red color (RGBA)
0, 0, // Min width/height (flexible)
1e9, 1e9, // Max width/height (very large)
)
yellowFill := widget.NewFlexFill(
1.0, 1.0, 0.0, 1.0, // Yellow color (RGBA)
0, 0, // Min width/height (flexible)
1e9, 1e9, // Max width/height (very large)
)
greenFill := widget.NewFlexFill(
0.0, 1.0, 0.0, 1.0, // Green color (RGBA)
0, 0, // Min width/height (flexible)
1e9, 1e9, // Max width/height (very large)
)
blueFill := widget.NewFlexFill(
0.0, 0.0, 1.0, 1.0, // Blue color (RGBA)
0, 0, // Min width/height (flexible)
1e9, 1e9, // Max width/height (very large)
)
// Create first row container (red and yellow)
topRow := widget.NewContainer(
widget.DirectionRow,
widget.NewFlexConstraints(0, 0, 1e9, 1e9), // Flexible constraints
)
topRow.AddChild(widget.NewFlexChild(redFill, 1.0)) // Equal weight
topRow.AddChild(widget.NewFlexChild(yellowFill, 1.0)) // Equal weight
// Create second row container (green and blue)
bottomRow := widget.NewContainer(
widget.DirectionRow,
widget.NewFlexConstraints(0, 0, 1e9, 1e9), // Flexible constraints
)
bottomRow.AddChild(widget.NewFlexChild(greenFill, 1.0)) // Equal weight
bottomRow.AddChild(widget.NewFlexChild(blueFill, 1.0)) // Equal weight
// Create main column container
mainColumn := widget.NewContainer(
widget.DirectionColumn,
widget.NewFlexConstraints(0, 0, 1e9, 1e9), // Flexible constraints
)
mainColumn.AddChild(widget.NewFlexChild(topRow, 1.0)) // Equal weight
mainColumn.AddChild(widget.NewFlexChild(bottomRow, 1.0)) // Equal weight
// Create a white box with fixed 64x64 size (no position needed)
// Using 0.5 alpha to test alpha blending
whiteBox := widget.NewRigidFill(
1.0, 1.0, 1.0, 0.75, // White with 0.75 alpha
64, 64, // Fixed 64x64 size
)
// Wrap the white box in a DirectionWidget with center gravity
centeredWhiteBox := widget.NewDirectionWidget(
whiteBox,
widget.GravityCenter,
widget.NewFlexConstraints(0, 0, 1e9, 1e9), // Flexible constraints to fill available space
)
// Create overlay widget to demonstrate overpainting
overlay := widget.NewOverlayWidget(
widget.NewFlexConstraints(0, 0, 1e9, 1e9), // Flexible constraints
)
// Add the flex layout first (background)
overlay.AddChild(mainColumn)
// Add the centered white box second (foreground - will paint over the flex layout)
overlay.AddChild(centeredWhiteBox)
// Create root widget with the overlay as child
app.rootWidget = widget.NewRootWidget(overlay)
return
}
// Render renders the widget tree
func (app *WidgetApp) Render(width, height int, mouseX, mouseY float64) (err error) {
// Set the clear color to black
gl.ClearColor(0.0, 0.0, 0.0, 1.0)
gl.Clear(gl.COLOR_BUFFER_BIT)
// Enable blending globally
gl.Enable(gl.BLEND)
gl.BlendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
gl.Enable(gl.SCISSOR_TEST)
// Set up the projection matrix for 2D rendering
// Use orthographic projection matching screen coordinates
gl.MatrixMode(gl.PROJECTION)
gl.LoadIdentity()
gl.Ortho(0, float64(width), 0, float64(height), -1, 1)
gl.MatrixMode(gl.MODELVIEW)
gl.LoadIdentity()
// Create widget context with window dimensions
widgetCtx := &interfaces.Context{
WindowWidth: width, // Window logical size
WindowHeight: height, // Window logical size
PaintedRegions: make([]interfaces.Rect, 0),
}
// Create a dummy box for the root widget
rootBox := &interfaces.Box{}
// Render the widget tree
_, err = app.rootWidget.Render(widgetCtx, rootBox)
if err != nil {
return
}
// Draw crosshair at mouse cursor position
drawCrosshair(float32(mouseX), float32(height)-float32(mouseY), width, height)
return
}
// drawCrosshair draws a 1-pixel wide black crosshair at the specified position
func drawCrosshair(x, y float32, width, height int) {
// Disable scissor test for crosshair to draw over everything
gl.Disable(gl.SCISSOR_TEST)
// Set line width to 1 pixel
gl.LineWidth(1.0)
// Set color to black
gl.Color4f(0.0, 0.0, 0.0, 1.0)
// Draw vertical line (full height)
gl.Begin(gl.LINES)
gl.Vertex2f(x, 0)
gl.Vertex2f(x, float32(height))
gl.End()
// Draw horizontal line (full width)
gl.Begin(gl.LINES)
gl.Vertex2f(0, y)
gl.Vertex2f(float32(width), y)
gl.End()
// Re-enable scissor test
gl.Enable(gl.SCISSOR_TEST)
}
func main() {
w, err := window.New(640, 480, "Fromage Widget Demo with GLFW")
if chk.E(err) {
return
}
app := &WidgetApp{}
if err := app.Init(); chk.E(err) {
return
}
if err := w.Run(app.Render); chk.E(err) {
return
}
}

13
go.mod Normal file
View File

@@ -0,0 +1,13 @@
module github.com/mleku/goo
go 1.24.0
toolchain go1.24.9
require (
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728
lol.mleku.dev v1.0.5
)
require github.com/davecgh/go-spew v1.1.1 // indirect

8
go.sum Normal file
View File

@@ -0,0 +1,8 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
lol.mleku.dev v1.0.5 h1:irwfwz+Scv74G/2OXmv05YFKOzUNOVZ735EAkYgjgM8=
lol.mleku.dev v1.0.5/go.mod h1:JlsqP0CZDLKRyd85XGcy79+ydSRqmFkrPzYFMYxQ+zs=

55
pkg/interfaces/widget.go Normal file
View File

@@ -0,0 +1,55 @@
package interfaces
// Point represents a 2D coordinate
type Point struct {
X, Y float32
}
// Size represents width and height dimensions
type Size struct {
Width, Height float32
}
// Constraints define minimum and maximum size limits and position within root widget
type Constraints struct {
MinWidth, MinHeight float32
MaxWidth, MaxHeight float32
// Top/Left coordinates relative to root widget (0,0 = top-left of canvas)
Top, Left float32
}
// Box represents the layout box for a widget with position and size
type Box struct {
// Position relative to parent's top-left corner
Position Point
// Actual size of the box
Size Size
// Size constraints
Constraints Constraints
}
// Rect represents a rectangular region
type Rect struct {
X, Y float32
Width, Height float32
}
// Context provides the rendering context for widgets
type Context struct {
// Window size
WindowWidth, WindowHeight int
// Parent box - widget's position is relative to this
ParentBox *Box
// Available space within parent
AvailableSize Size
// Painted regions to avoid double painting
PaintedRegions []Rect
}
// Widget defines the interface that all widgets must implement
type Widget interface {
// Render draws the widget within the given box and returns the actual size used
Render(ctx *Context, box *Box) (usedSize Size, err error)
// GetConstraints returns the size constraints for this widget
GetConstraints() Constraints
}

10
pkg/widget/errors.go Normal file
View File

@@ -0,0 +1,10 @@
package widget
import (
"errors"
)
var (
// errInvalidDirection is returned when an invalid layout direction is specified
errInvalidDirection = errors.New("invalid direction")
)

92
pkg/widget/fill.go Normal file
View File

@@ -0,0 +1,92 @@
package widget
import (
"github.com/go-gl/gl/all-core/gl"
)
// Fill is a widget that fills its box with a solid color
type Fill struct {
color [4]float32
constraints Constraints
}
// NewFill creates a new Fill widget with the specified color and constraints
func NewFill(red, green, blue, alpha float32, constraints Constraints) *Fill {
return &Fill{
color: [4]float32{red, green, blue, alpha},
constraints: constraints,
}
}
// NewRigidFill creates a rigid Fill widget with fixed dimensions
func NewRigidFill(red, green, blue, alpha, width, height float32) *Fill {
return &Fill{
color: [4]float32{red, green, blue, alpha},
constraints: NewRigidConstraints(width, height),
}
}
// NewFlexFill creates a flexible Fill widget with min/max constraints
func NewFlexFill(red, green, blue, alpha, minWidth, minHeight, maxWidth, maxHeight float32) *Fill {
return &Fill{
color: [4]float32{red, green, blue, alpha},
constraints: NewFlexConstraints(minWidth, minHeight, maxWidth, maxHeight),
}
}
// NewFlexFillAt creates a flexible Fill widget at a specific position
func NewFlexFillAt(red, green, blue, alpha, minWidth, minHeight, maxWidth, maxHeight, top, left float32) *Fill {
return &Fill{
color: [4]float32{red, green, blue, alpha},
constraints: NewFlexConstraintsAt(minWidth, minHeight, maxWidth, maxHeight, top, left),
}
}
// NewRigidFillAt creates a rigid Fill widget at a specific position
func NewRigidFillAt(red, green, blue, alpha, width, height, top, left float32) *Fill {
return &Fill{
color: [4]float32{red, green, blue, alpha},
constraints: NewRigidConstraintsAt(width, height, top, left),
}
}
// SetColor updates the fill color
func (f *Fill) SetColor(red, green, blue, alpha float32) {
f.color = [4]float32{red, green, blue, alpha}
}
// GetConstraints returns the size constraints for this Fill widget
func (f *Fill) GetConstraints() Constraints {
return f.constraints
}
// Render implements the Widget interface for Fill
func (f *Fill) Render(ctx *Context, box *Box) (usedSize Size, err error) {
// Set scissor test to clip to the box
// Convert from GL coordinates (bottom-left origin) to screen coordinates (top-left origin)
// Window height is ctx.WindowHeight, box Y is from top
scissorX := int32(box.Position.X)
scissorY := int32(float32(ctx.WindowHeight) - box.Position.Y - box.Size.Height)
scissorW := int32(box.Size.Width)
scissorH := int32(box.Size.Height)
gl.Scissor(scissorX, scissorY, scissorW, scissorH)
// Set the color
gl.Color4f(f.color[0], f.color[1], f.color[2], f.color[3])
// Create vertices for the quad
x1, y1 := box.Position.X, float32(ctx.WindowHeight)-box.Position.Y
x2, y2 := box.Position.X+box.Size.Width, float32(ctx.WindowHeight)-box.Position.Y
x3, y3 := box.Position.X+box.Size.Width, float32(ctx.WindowHeight)-box.Position.Y-box.Size.Height
x4, y4 := box.Position.X, float32(ctx.WindowHeight)-box.Position.Y-box.Size.Height
// Draw using immediate mode
gl.Begin(gl.QUADS)
gl.Vertex2f(x1, y1)
gl.Vertex2f(x2, y2)
gl.Vertex2f(x3, y3)
gl.Vertex2f(x4, y4)
gl.End()
return box.Size, nil
}

681
pkg/widget/widget.go Normal file
View File

@@ -0,0 +1,681 @@
package widget
import (
"github.com/mleku/goo/pkg/interfaces"
"lol.mleku.dev/chk"
)
// Re-export types from interfaces package for convenience
type (
Point = interfaces.Point
Size = interfaces.Size
Constraints = interfaces.Constraints
Box = interfaces.Box
Context = interfaces.Context
Widget = interfaces.Widget
)
// NewConstraints creates constraints with min/max values and position
func NewConstraints(minWidth, minHeight, maxWidth, maxHeight, top, left float32) Constraints {
return Constraints{
MinWidth: minWidth,
MinHeight: minHeight,
MaxWidth: maxWidth,
MaxHeight: maxHeight,
Top: top,
Left: left,
}
}
// NewConstraintsNoPos creates constraints with min/max values and no specific position
func NewConstraintsNoPos(minWidth, minHeight, maxWidth, maxHeight float32) Constraints {
return Constraints{
MinWidth: minWidth,
MinHeight: minHeight,
MaxWidth: maxWidth,
MaxHeight: maxHeight,
Top: 0,
Left: 0,
}
}
// NewRigidConstraints creates constraints for a fixed size (rigid widget)
func NewRigidConstraints(width, height float32) Constraints {
return Constraints{
MinWidth: width,
MinHeight: height,
MaxWidth: width,
MaxHeight: height,
Top: 0,
Left: 0,
}
}
// NewRigidConstraintsAt creates constraints for a fixed size at specific position
func NewRigidConstraintsAt(width, height, top, left float32) Constraints {
return Constraints{
MinWidth: width,
MinHeight: height,
MaxWidth: width,
MaxHeight: height,
Top: top,
Left: left,
}
}
// NewFlexConstraints creates constraints for a flexible widget
func NewFlexConstraints(minWidth, minHeight, maxWidth, maxHeight float32) Constraints {
return Constraints{
MinWidth: minWidth,
MinHeight: minHeight,
MaxWidth: maxWidth,
MaxHeight: maxHeight,
Top: 0,
Left: 0,
}
}
// NewFlexConstraintsAt creates constraints for a flexible widget at specific position
func NewFlexConstraintsAt(minWidth, minHeight, maxWidth, maxHeight, top, left float32) Constraints {
return Constraints{
MinWidth: minWidth,
MinHeight: minHeight,
MaxWidth: maxWidth,
MaxHeight: maxHeight,
Top: top,
Left: left,
}
}
// NewBox creates a new box with the given position, size, and constraints
func NewBox(x, y, width, height float32, constraints Constraints) *Box {
return &Box{
Position: Point{X: x, Y: y},
Size: Size{Width: width, Height: height},
Constraints: constraints,
}
}
// Direction specifies layout direction for containers
type Direction int
const (
DirectionRow Direction = iota
DirectionColumn
)
// FlexType specifies whether a widget is rigid or flexible
type FlexType int
const (
FlexTypeRigid FlexType = iota
FlexTypeFlex
)
// FlexChild represents a child widget in a flex container
type FlexChild struct {
Widget Widget
Type FlexType
Weight float32 // Only used for FlexTypeFlex
}
// NewRigidChild creates a rigid child widget
func NewRigidChild(widget Widget) FlexChild {
return FlexChild{
Widget: widget,
Type: FlexTypeRigid,
Weight: 0,
}
}
// NewFlexChild creates a flexible child widget with the given weight
func NewFlexChild(widget Widget, weight float32) FlexChild {
return FlexChild{
Widget: widget,
Type: FlexTypeFlex,
Weight: weight,
}
}
// Container is a widget that lays out children in rows or columns
type Container struct {
Direction Direction
Children []FlexChild
constraints Constraints
}
// NewContainer creates a new container with the specified direction
func NewContainer(direction Direction, constraints Constraints) *Container {
return &Container{
Direction: direction,
Children: make([]FlexChild, 0),
constraints: constraints,
}
}
// AddChild adds a child widget to the container
func (c *Container) AddChild(child FlexChild) {
c.Children = append(c.Children, child)
}
// GetConstraints returns the container's constraints
func (c *Container) GetConstraints() Constraints {
return c.constraints
}
// Render implements the Widget interface for Container
func (c *Container) Render(ctx *Context, box *Box) (usedSize Size, err error) {
if len(c.Children) == 0 {
return Size{}, nil
}
// Calculate layout based on direction
switch c.Direction {
case DirectionRow:
return c.renderRow(ctx, box)
case DirectionColumn:
return c.renderColumn(ctx, box)
default:
return Size{}, errInvalidDirection
}
}
// renderRow lays out children horizontally
func (c *Container) renderRow(ctx *Context, box *Box) (usedSize Size, err error) {
availableWidth := box.Size.Width
availableHeight := box.Size.Height
// First pass: calculate rigid sizes and total flex weight
var rigidWidth float32
var totalFlexWeight float32
var maxHeight float32
for _, child := range c.Children {
childConstraints := child.Widget.GetConstraints()
if child.Type == FlexTypeRigid {
rigidWidth += childConstraints.MinWidth
if childConstraints.MinHeight > maxHeight {
maxHeight = childConstraints.MinHeight
}
} else {
totalFlexWeight += child.Weight
if childConstraints.MinHeight > maxHeight {
maxHeight = childConstraints.MinHeight
}
}
}
// Calculate remaining width for flex children
flexWidth := availableWidth - rigidWidth
if flexWidth < 0 {
flexWidth = 0
}
// Second pass: render children
var currentX float32
var actualUsedWidth float32
var actualMaxHeight float32
for _, child := range c.Children {
childConstraints := child.Widget.GetConstraints()
var childWidth float32
if child.Type == FlexTypeRigid {
childWidth = childConstraints.MinWidth
} else {
if totalFlexWeight > 0 {
childWidth = (flexWidth * child.Weight) / totalFlexWeight
// Clamp to constraints
if childWidth < childConstraints.MinWidth {
childWidth = childConstraints.MinWidth
}
if childWidth > childConstraints.MaxWidth {
childWidth = childConstraints.MaxWidth
}
} else {
childWidth = childConstraints.MinWidth
}
}
// Create child box
childBox := &Box{
Position: Point{
X: box.Position.X + currentX,
Y: box.Position.Y,
},
Size: Size{
Width: childWidth,
Height: availableHeight,
},
Constraints: childConstraints,
}
// Create child context
childCtx := &Context{
WindowWidth: ctx.WindowWidth,
WindowHeight: ctx.WindowHeight,
ParentBox: childBox,
AvailableSize: childBox.Size,
}
// Render child
childUsedSize, err := child.Widget.Render(childCtx, childBox)
if chk.E(err) {
return Size{}, err
}
currentX += childUsedSize.Width
actualUsedWidth += childUsedSize.Width
if childUsedSize.Height > actualMaxHeight {
actualMaxHeight = childUsedSize.Height
}
}
return Size{Width: actualUsedWidth, Height: actualMaxHeight}, nil
}
// renderColumn lays out children vertically
func (c *Container) renderColumn(ctx *Context, box *Box) (usedSize Size, err error) {
availableWidth := box.Size.Width
availableHeight := box.Size.Height
// First pass: calculate rigid sizes and total flex weight
var rigidHeight float32
var totalFlexWeight float32
var maxWidth float32
for _, child := range c.Children {
childConstraints := child.Widget.GetConstraints()
if child.Type == FlexTypeRigid {
rigidHeight += childConstraints.MinHeight
if childConstraints.MinWidth > maxWidth {
maxWidth = childConstraints.MinWidth
}
} else {
totalFlexWeight += child.Weight
if childConstraints.MinWidth > maxWidth {
maxWidth = childConstraints.MinWidth
}
}
}
// Calculate remaining height for flex children
flexHeight := availableHeight - rigidHeight
if flexHeight < 0 {
flexHeight = 0
}
// Second pass: render children
var currentY float32
var actualUsedHeight float32
var actualMaxWidth float32
for _, child := range c.Children {
childConstraints := child.Widget.GetConstraints()
var childHeight float32
if child.Type == FlexTypeRigid {
childHeight = childConstraints.MinHeight
} else {
if totalFlexWeight > 0 {
childHeight = (flexHeight * child.Weight) / totalFlexWeight
// Clamp to constraints
if childHeight < childConstraints.MinHeight {
childHeight = childConstraints.MinHeight
}
if childHeight > childConstraints.MaxHeight {
childHeight = childConstraints.MaxHeight
}
} else {
childHeight = childConstraints.MinHeight
}
}
// Create child box
childBox := &Box{
Position: Point{
X: box.Position.X,
Y: box.Position.Y + currentY,
},
Size: Size{
Width: availableWidth,
Height: childHeight,
},
Constraints: childConstraints,
}
// Create child context
childCtx := &Context{
WindowWidth: ctx.WindowWidth,
WindowHeight: ctx.WindowHeight,
ParentBox: childBox,
AvailableSize: childBox.Size,
}
// Render child
childUsedSize, err := child.Widget.Render(childCtx, childBox)
if chk.E(err) {
return Size{}, err
}
currentY += childUsedSize.Height
actualUsedHeight += childUsedSize.Height
if childUsedSize.Width > actualMaxWidth {
actualMaxWidth = childUsedSize.Width
}
}
return Size{Width: actualMaxWidth, Height: actualUsedHeight}, nil
}
// RootWidget manages the root layout that spans the entire canvas
type RootWidget struct {
child Widget
clearColor [4]float32
}
// NewRootWidget creates a new root widget with the given child
func NewRootWidget(child Widget) *RootWidget {
return &RootWidget{
child: child,
clearColor: [4]float32{0.0, 0.0, 0.0, 1.0}, // Default black
}
}
// SetClearColor sets the background clear color for the root widget
func (r *RootWidget) SetClearColor(red, green, blue, alpha float32) {
r.clearColor = [4]float32{red, green, blue, alpha}
}
// GetConstraints returns unconstrained size (fills canvas)
func (r *RootWidget) GetConstraints() Constraints {
return Constraints{
MinWidth: 0,
MinHeight: 0,
MaxWidth: 1e9, // Very large number
MaxHeight: 1e9,
}
}
// Render implements the Widget interface for RootWidget
func (r *RootWidget) Render(ctx *Context, box *Box) (usedSize Size, err error) {
if r.child == nil {
return box.Size, nil
}
// Get child constraints to determine positioning
childConstraints := r.child.GetConstraints()
// Create a box that spans the entire canvas, but position child based on its constraints
canvasWidth := float32(ctx.WindowWidth)
canvasHeight := float32(ctx.WindowHeight)
// Use constraint coordinates if specified, otherwise fill canvas
childBox := &Box{
Position: Point{
X: childConstraints.Left,
Y: childConstraints.Top,
},
Size: Size{
Width: canvasWidth - childConstraints.Left,
Height: canvasHeight - childConstraints.Top,
},
Constraints: childConstraints,
}
// If child has specific size constraints, respect them
if childConstraints.MaxWidth < childBox.Size.Width {
childBox.Size.Width = childConstraints.MaxWidth
}
if childConstraints.MaxHeight < childBox.Size.Height {
childBox.Size.Height = childConstraints.MaxHeight
}
if childConstraints.MinWidth > childBox.Size.Width {
childBox.Size.Width = childConstraints.MinWidth
}
if childConstraints.MinHeight > childBox.Size.Height {
childBox.Size.Height = childConstraints.MinHeight
}
// Create context for child
childCtx := &Context{
WindowWidth: ctx.WindowWidth,
WindowHeight: ctx.WindowHeight,
ParentBox: childBox,
AvailableSize: childBox.Size,
}
// Render child
return r.child.Render(childCtx, childBox)
}
// OverlayWidget allows multiple widgets to be rendered on top of each other
type OverlayWidget struct {
children []Widget
constraints Constraints
}
// NewOverlayWidget creates a new overlay widget that renders children in sequence
func NewOverlayWidget(constraints Constraints) *OverlayWidget {
return &OverlayWidget{
children: make([]Widget, 0),
constraints: constraints,
}
}
// AddChild adds a child widget to be rendered on top of previous children
func (o *OverlayWidget) AddChild(child Widget) {
o.children = append(o.children, child)
}
// GetConstraints returns the overlay's constraints
func (o *OverlayWidget) GetConstraints() Constraints {
return o.constraints
}
// Render implements the Widget interface for OverlayWidget
func (o *OverlayWidget) Render(ctx *Context, box *Box) (usedSize Size, err error) {
var maxUsedSize Size
// Render all children in sequence (later children paint over earlier ones)
for _, child := range o.children {
// Get child constraints to determine positioning and sizing
childConstraints := child.GetConstraints()
// Create child box based on its constraints
childBox := &Box{
Position: Point{
X: box.Position.X + childConstraints.Left,
Y: box.Position.Y + childConstraints.Top,
},
Size: Size{
Width: box.Size.Width - childConstraints.Left,
Height: box.Size.Height - childConstraints.Top,
},
Constraints: childConstraints,
}
// For rigid widgets (min == max), use the exact constraint size
// For flexible widgets, clamp to available space within constraints
if childConstraints.MinWidth == childConstraints.MaxWidth {
// Rigid width
childBox.Size.Width = childConstraints.MinWidth
} else {
// Flexible width - clamp to constraints
if childConstraints.MaxWidth < childBox.Size.Width {
childBox.Size.Width = childConstraints.MaxWidth
}
if childConstraints.MinWidth > childBox.Size.Width {
childBox.Size.Width = childConstraints.MinWidth
}
}
if childConstraints.MinHeight == childConstraints.MaxHeight {
// Rigid height
childBox.Size.Height = childConstraints.MinHeight
} else {
// Flexible height - clamp to constraints
if childConstraints.MaxHeight < childBox.Size.Height {
childBox.Size.Height = childConstraints.MaxHeight
}
if childConstraints.MinHeight > childBox.Size.Height {
childBox.Size.Height = childConstraints.MinHeight
}
}
// Create child context
childCtx := &Context{
WindowWidth: ctx.WindowWidth,
WindowHeight: ctx.WindowHeight,
ParentBox: childBox,
AvailableSize: childBox.Size,
}
childUsedSize, err := child.Render(childCtx, childBox)
if chk.E(err) {
return Size{}, err
}
// Track the maximum used size
if childUsedSize.Width > maxUsedSize.Width {
maxUsedSize.Width = childUsedSize.Width
}
if childUsedSize.Height > maxUsedSize.Height {
maxUsedSize.Height = childUsedSize.Height
}
}
return maxUsedSize, nil
}
// Gravity specifies how a widget should be positioned within its container
type Gravity int
const (
GravityCenter Gravity = iota
GravityNorth
GravitySouth
GravityEast
GravityWest
GravityNorthEast
GravityNorthWest
GravitySouthEast
GravitySouthWest
)
// DirectionWidget positions a single child widget using gravity-based positioning
type DirectionWidget struct {
child Widget
gravity Gravity
constraints Constraints
}
// NewDirectionWidget creates a new direction widget with the specified gravity
func NewDirectionWidget(child Widget, gravity Gravity, constraints Constraints) *DirectionWidget {
return &DirectionWidget{
child: child,
gravity: gravity,
constraints: constraints,
}
}
// GetConstraints returns the direction widget's constraints
func (d *DirectionWidget) GetConstraints() Constraints {
return d.constraints
}
// Render implements the Widget interface for DirectionWidget
func (d *DirectionWidget) Render(ctx *Context, box *Box) (usedSize Size, err error) {
if d.child == nil {
return box.Size, nil
}
// Get child constraints
childConstraints := d.child.GetConstraints()
// Calculate child size (respecting rigid constraints)
var childWidth, childHeight float32
if childConstraints.MinWidth == childConstraints.MaxWidth {
childWidth = childConstraints.MinWidth
} else {
childWidth = box.Size.Width
if childWidth > childConstraints.MaxWidth {
childWidth = childConstraints.MaxWidth
}
if childWidth < childConstraints.MinWidth {
childWidth = childConstraints.MinWidth
}
}
if childConstraints.MinHeight == childConstraints.MaxHeight {
childHeight = childConstraints.MinHeight
} else {
childHeight = box.Size.Height
if childHeight > childConstraints.MaxHeight {
childHeight = childConstraints.MaxHeight
}
if childHeight < childConstraints.MinHeight {
childHeight = childConstraints.MinHeight
}
}
// Calculate position based on gravity
var childX, childY float32
switch d.gravity {
case GravityCenter:
childX = box.Position.X + (box.Size.Width-childWidth)/2
childY = box.Position.Y + (box.Size.Height-childHeight)/2
case GravityNorth:
childX = box.Position.X + (box.Size.Width-childWidth)/2
childY = box.Position.Y
case GravitySouth:
childX = box.Position.X + (box.Size.Width-childWidth)/2
childY = box.Position.Y + box.Size.Height - childHeight
case GravityEast:
childX = box.Position.X + box.Size.Width - childWidth
childY = box.Position.Y + (box.Size.Height-childHeight)/2
case GravityWest:
childX = box.Position.X
childY = box.Position.Y + (box.Size.Height-childHeight)/2
case GravityNorthEast:
childX = box.Position.X + box.Size.Width - childWidth
childY = box.Position.Y
case GravityNorthWest:
childX = box.Position.X
childY = box.Position.Y
case GravitySouthEast:
childX = box.Position.X + box.Size.Width - childWidth
childY = box.Position.Y + box.Size.Height - childHeight
case GravitySouthWest:
childX = box.Position.X
childY = box.Position.Y + box.Size.Height - childHeight
}
// Create child box
childBox := &Box{
Position: Point{
X: childX,
Y: childY,
},
Size: Size{
Width: childWidth,
Height: childHeight,
},
Constraints: childConstraints,
}
// Create child context
childCtx := &Context{
WindowWidth: ctx.WindowWidth,
WindowHeight: ctx.WindowHeight,
ParentBox: childBox,
AvailableSize: childBox.Size,
}
// Render child
return d.child.Render(childCtx, childBox)
}

154
pkg/window/window.go Normal file
View File

@@ -0,0 +1,154 @@
package window
import (
"runtime"
"github.com/go-gl/gl/all-core/gl"
"github.com/go-gl/glfw/v3.3/glfw"
"lol.mleku.dev/chk"
"lol.mleku.dev/log"
)
// Window manages the OpenGL window and application lifecycle
type Window struct {
width int
height int
title string
window *glfw.Window
running bool
canvasWidth int
canvasHeight int
frameCount int
skipResizeFrames bool
resizeThreshold int
mouseX float64
mouseY float64
}
func init() {
runtime.LockOSThread()
}
// New creates a new window with the given configuration
func New(width, height int, title string) (w *Window, err error) {
w = &Window{
width: width,
height: height,
title: title,
canvasWidth: width,
canvasHeight: height,
resizeThreshold: 8,
skipResizeFrames: true,
}
return
}
// Run starts the window and runs the application main loop
func (w *Window) Run(renderFunc func(windowWidth, windowHeight int, mouseX, mouseY float64) error) (err error) {
if err = glfw.Init(); chk.E(err) {
return
}
defer glfw.Terminate()
glfw.WindowHint(glfw.ContextVersionMajor, 2)
glfw.WindowHint(glfw.ContextVersionMinor, 1)
// Don't set OpenGLProfile - use compatibility profile for immediate mode
glfw.WindowHint(glfw.Resizable, glfw.True)
w.window, err = glfw.CreateWindow(w.width, w.height, w.title, nil, nil)
if chk.E(err) {
return
}
defer w.window.Destroy()
w.window.MakeContextCurrent()
if err = gl.Init(); chk.E(err) {
return
}
// Set the viewport
gl.Viewport(0, 0, int32(w.width), int32(w.height))
// Enable scissor test for clipping
gl.Enable(gl.SCISSOR_TEST)
// Initialize canvas dimensions
w.canvasWidth, w.canvasHeight = w.window.GetFramebufferSize()
// Set mouse cursor position callback
w.window.SetCursorPosCallback(func(window *glfw.Window, xpos, ypos float64) {
w.mouseX = xpos
w.mouseY = ypos
log.D.Ln("Cursor position:", xpos, ypos)
})
// Set keyboard callback
w.window.SetKeyCallback(func(window *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
log.D.Ln("Key event: key=", key, "scancode=", scancode, "action=", action, "mods=", mods)
})
// Set mouse button callback
w.window.SetMouseButtonCallback(func(window *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
log.D.Ln("Mouse button: button=", button, "action=", action, "mods=", mods)
})
// Set scroll callback
w.window.SetScrollCallback(func(window *glfw.Window, xoffset, yoffset float64) {
log.D.Ln("Scroll: xoffset=", xoffset, "yoffset=", yoffset)
})
// Set character input callback
w.window.SetCharCallback(func(window *glfw.Window, char rune) {
log.D.Ln("Character input:", string(char))
})
w.running = true
for !w.window.ShouldClose() && w.running {
// Get window size (logical size in screen coordinates)
windowWidth, windowHeight := w.window.GetSize()
// Get framebuffer/canvas size (actual rendering surface)
canvasWidth, canvasHeight := w.window.GetFramebufferSize()
// Increment frame counter
w.frameCount++
// Update viewport if canvas size changed
if canvasWidth != w.canvasWidth || canvasHeight != w.canvasHeight {
gl.Viewport(0, 0, int32(canvasWidth), int32(canvasHeight))
w.canvasWidth = canvasWidth
w.canvasHeight = canvasHeight
}
// Render with window dimensions and mouse position
if err = renderFunc(windowWidth, windowHeight, w.mouseX, w.mouseY); chk.E(err) {
return
}
w.window.SwapBuffers()
glfw.PollEvents()
}
return
}
// Stop stops the main loop
func (w *Window) Stop() {
w.running = false
}
// GetWindow returns the underlying GLFW window
func (w *Window) GetWindow() *glfw.Window {
return w.window
}
// abs returns the absolute value of an integer
func abs(x int) int {
if x < 0 {
return -x
}
return x
}