Initial commit: Add Go module, basic window management, and widget framework with fill and layout capabilities.
This commit is contained in:
180
cmd/hello/main.go
Normal file
180
cmd/hello/main.go
Normal 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
13
go.mod
Normal 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
8
go.sum
Normal 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
55
pkg/interfaces/widget.go
Normal 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
10
pkg/widget/errors.go
Normal 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
92
pkg/widget/fill.go
Normal 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
681
pkg/widget/widget.go
Normal 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
154
pkg/window/window.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user