diff --git a/cmd/hello/main.go b/cmd/hello/main.go new file mode 100644 index 0000000..e627286 --- /dev/null +++ b/cmd/hello/main.go @@ -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 + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..419f57d --- /dev/null +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..672f96c --- /dev/null +++ b/go.sum @@ -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= diff --git a/pkg/interfaces/widget.go b/pkg/interfaces/widget.go new file mode 100644 index 0000000..3e66671 --- /dev/null +++ b/pkg/interfaces/widget.go @@ -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 +} diff --git a/pkg/widget/errors.go b/pkg/widget/errors.go new file mode 100644 index 0000000..738741e --- /dev/null +++ b/pkg/widget/errors.go @@ -0,0 +1,10 @@ +package widget + +import ( + "errors" +) + +var ( + // errInvalidDirection is returned when an invalid layout direction is specified + errInvalidDirection = errors.New("invalid direction") +) diff --git a/pkg/widget/fill.go b/pkg/widget/fill.go new file mode 100644 index 0000000..9a8f9de --- /dev/null +++ b/pkg/widget/fill.go @@ -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 +} diff --git a/pkg/widget/widget.go b/pkg/widget/widget.go new file mode 100644 index 0000000..02bfb2e --- /dev/null +++ b/pkg/widget/widget.go @@ -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) +} diff --git a/pkg/window/window.go b/pkg/window/window.go new file mode 100644 index 0000000..894e1b2 --- /dev/null +++ b/pkg/window/window.go @@ -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 +}