making lots of progress!
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
// Package layout provides a simple API for creating text-based layouts
|
||||
// that can be rendered to fbdraw grids for display in a carousel.
|
||||
//
|
||||
//nolint:mnd
|
||||
package layout
|
||||
|
||||
import (
|
||||
@@ -22,6 +24,20 @@ const (
|
||||
SourceCodePro
|
||||
)
|
||||
|
||||
// String implements the Stringer interface for Font
|
||||
func (f Font) String() string {
|
||||
switch f {
|
||||
case PlexMono:
|
||||
return "PlexMono"
|
||||
case Terminus:
|
||||
return "Terminus"
|
||||
case SourceCodePro:
|
||||
return "SourceCodePro"
|
||||
default:
|
||||
return fmt.Sprintf("Font(%d)", int(f))
|
||||
}
|
||||
}
|
||||
|
||||
// Color returns a standard color by name
|
||||
func Color(name string) color.Color {
|
||||
switch name {
|
||||
@@ -69,15 +85,14 @@ func Color(name string) color.Color {
|
||||
}
|
||||
|
||||
// Draw provides the drawing context for creating a layout.
|
||||
// It maintains state for font, size, colors, and text styling.
|
||||
// It maintains state for colors and text styling.
|
||||
type Draw struct {
|
||||
// Drawing state
|
||||
font Font
|
||||
fontSize int
|
||||
bold bool
|
||||
italic bool
|
||||
fgColor color.Color
|
||||
bgColor color.Color
|
||||
font Font
|
||||
bold bool
|
||||
italic bool
|
||||
fgColor color.Color
|
||||
bgColor color.Color
|
||||
|
||||
// Grid to render to
|
||||
grid *fbdraw.CharGrid
|
||||
@@ -87,22 +102,27 @@ type Draw struct {
|
||||
Height int
|
||||
}
|
||||
|
||||
// NewDraw creates a new drawing context with the specified dimensions
|
||||
func NewDraw(width, height int) *Draw {
|
||||
grid := fbdraw.NewCharGrid(width, height)
|
||||
return &Draw{
|
||||
grid: grid,
|
||||
Width: width,
|
||||
Height: height,
|
||||
fontSize: 14,
|
||||
fgColor: color.RGBA{255, 255, 255, 255},
|
||||
bgColor: color.RGBA{0, 0, 0, 255},
|
||||
}
|
||||
// String implements the Stringer interface for Draw
|
||||
func (d *Draw) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Draw{Width:%d, Height:%d, Font:%v, Bold:%v, Italic:%v}",
|
||||
d.Width,
|
||||
d.Height,
|
||||
d.font,
|
||||
d.bold,
|
||||
d.italic,
|
||||
)
|
||||
}
|
||||
|
||||
// Render returns the current grid for rendering by the carousel
|
||||
func (d *Draw) Render() *fbdraw.CharGrid {
|
||||
return d.grid
|
||||
// NewDraw creates a new drawing context that will modify the provided grid
|
||||
func NewDraw(grid *fbdraw.CharGrid) *Draw {
|
||||
return &Draw{
|
||||
grid: grid,
|
||||
Width: grid.Width,
|
||||
Height: grid.Height,
|
||||
fgColor: color.RGBA{255, 255, 255, 255},
|
||||
bgColor: color.RGBA{0, 0, 0, 255},
|
||||
}
|
||||
}
|
||||
|
||||
// Clear fills the entire display with black.
|
||||
@@ -130,13 +150,6 @@ func (d *Draw) Font(f Font) *Draw {
|
||||
return d
|
||||
}
|
||||
|
||||
// Size sets the current font size in points.
|
||||
func (d *Draw) Size(points int) *Draw {
|
||||
d.fontSize = points
|
||||
d.grid.FontSize = float64(points)
|
||||
return d
|
||||
}
|
||||
|
||||
// Bold enables bold text rendering.
|
||||
func (d *Draw) Bold() *Draw {
|
||||
d.bold = true
|
||||
@@ -181,15 +194,18 @@ func (d *Draw) Text(x, y int, format string, args ...interface{}) {
|
||||
writer.SetWeight(font.WeightRegular)
|
||||
}
|
||||
writer.SetItalic(d.italic)
|
||||
writer.Write(text)
|
||||
writer.Write("%s", text)
|
||||
}
|
||||
|
||||
// TextCenter draws centered text at the specified y coordinate.
|
||||
func (d *Draw) TextCenter(x, y int, format string, args ...interface{}) {
|
||||
text := fmt.Sprintf(format, args...)
|
||||
// Calculate starting position for centered text
|
||||
startX := x + (d.Width-len(text))/2
|
||||
d.Text(startX, y, text)
|
||||
startX := (d.Width - len(text)) / 2
|
||||
if startX < 0 {
|
||||
startX = 0
|
||||
}
|
||||
d.Text(startX, y, "%s", text)
|
||||
}
|
||||
|
||||
// Grid creates a text grid region for simplified text layout.
|
||||
@@ -218,6 +234,18 @@ type Grid struct {
|
||||
hasBorder bool
|
||||
}
|
||||
|
||||
// String implements the Stringer interface for Grid
|
||||
func (g *Grid) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Grid{Pos:(%d,%d), Size:%dx%d, Border:%v}",
|
||||
g.x,
|
||||
g.y,
|
||||
g.cols,
|
||||
g.rows,
|
||||
g.hasBorder,
|
||||
)
|
||||
}
|
||||
|
||||
// Write places text at the specified row and column within the grid.
|
||||
// Text that exceeds the grid bounds is clipped.
|
||||
func (g *Grid) Write(col, row int, format string, args ...interface{}) {
|
||||
@@ -225,11 +253,11 @@ func (g *Grid) Write(col, row int, format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
text := fmt.Sprintf(format, args...)
|
||||
|
||||
|
||||
// Calculate absolute position
|
||||
absX := g.x + col
|
||||
absY := g.y + row
|
||||
|
||||
|
||||
// Write text with clipping
|
||||
writer := fbdraw.NewGridWriter(g.draw.grid)
|
||||
writer.MoveAbs(absX, absY)
|
||||
@@ -243,13 +271,13 @@ func (g *Grid) Write(col, row int, format string, args ...interface{}) {
|
||||
} else {
|
||||
writer.SetBackground(g.draw.bgColor)
|
||||
}
|
||||
|
||||
|
||||
// Clip text to grid bounds
|
||||
maxLen := g.cols - col
|
||||
if len(text) > maxLen {
|
||||
text = text[:maxLen]
|
||||
}
|
||||
writer.Write(text)
|
||||
writer.Write("%s", text)
|
||||
}
|
||||
|
||||
// WriteCenter centers text within the specified row.
|
||||
@@ -262,7 +290,7 @@ func (g *Grid) WriteCenter(row int, format string, args ...interface{}) {
|
||||
if col < 0 {
|
||||
col = 0
|
||||
}
|
||||
g.Write(col, row, text)
|
||||
g.Write(col, row, "%s", text)
|
||||
}
|
||||
|
||||
// Color sets the foreground color for subsequent Write operations.
|
||||
@@ -281,7 +309,15 @@ func (g *Grid) Background(c color.Color) *Grid {
|
||||
if g.draw.bold {
|
||||
weight = font.WeightBold
|
||||
}
|
||||
g.draw.grid.SetCell(g.x+col, g.y+row, ' ', g.draw.fgColor, c, weight, g.draw.italic)
|
||||
g.draw.grid.SetCell(
|
||||
g.x+col,
|
||||
g.y+row,
|
||||
' ',
|
||||
g.draw.fgColor,
|
||||
c,
|
||||
weight,
|
||||
g.draw.italic,
|
||||
)
|
||||
}
|
||||
}
|
||||
return g
|
||||
@@ -294,7 +330,7 @@ func (g *Grid) Border(c color.Color) *Grid {
|
||||
// Draw border using box drawing characters
|
||||
writer := fbdraw.NewGridWriter(g.draw.grid)
|
||||
writer.SetColor(c)
|
||||
|
||||
|
||||
// Top border
|
||||
writer.MoveAbs(g.x-1, g.y-1)
|
||||
writer.Write("┌")
|
||||
@@ -302,7 +338,7 @@ func (g *Grid) Border(c color.Color) *Grid {
|
||||
writer.Write("─")
|
||||
}
|
||||
writer.Write("┐")
|
||||
|
||||
|
||||
// Side borders
|
||||
for row := 0; row < g.rows; row++ {
|
||||
writer.MoveAbs(g.x-1, g.y+row)
|
||||
@@ -310,7 +346,7 @@ func (g *Grid) Border(c color.Color) *Grid {
|
||||
writer.MoveAbs(g.x+g.cols, g.y+row)
|
||||
writer.Write("│")
|
||||
}
|
||||
|
||||
|
||||
// Bottom border
|
||||
writer.MoveAbs(g.x-1, g.y+g.rows)
|
||||
writer.Write("└")
|
||||
@@ -318,7 +354,7 @@ func (g *Grid) Border(c color.Color) *Grid {
|
||||
writer.Write("─")
|
||||
}
|
||||
writer.Write("┘")
|
||||
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
@@ -328,7 +364,15 @@ func (g *Grid) RowBackground(row int, c color.Color) {
|
||||
return
|
||||
}
|
||||
for col := 0; col < g.cols; col++ {
|
||||
g.draw.grid.SetCell(g.x+col, g.y+row, ' ', g.draw.fgColor, c, font.WeightRegular, false)
|
||||
g.draw.grid.SetCell(
|
||||
g.x+col,
|
||||
g.y+row,
|
||||
' ',
|
||||
g.draw.fgColor,
|
||||
c,
|
||||
font.WeightRegular,
|
||||
false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,13 +397,13 @@ func (g *Grid) Bar(col, row, width int, percent float64, c color.Color) {
|
||||
if row < 0 || row >= g.rows || col < 0 || col >= g.cols {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Ensure width doesn't exceed grid bounds
|
||||
maxWidth := g.cols - col
|
||||
if width > maxWidth {
|
||||
width = maxWidth
|
||||
}
|
||||
|
||||
|
||||
writer := fbdraw.NewGridWriter(g.draw.grid)
|
||||
writer.MoveAbs(g.x+col, g.y+row)
|
||||
writer.SetColor(c)
|
||||
@@ -388,10 +432,10 @@ func Meter(percent float64, width int) string {
|
||||
if percent > 100 {
|
||||
percent = 100
|
||||
}
|
||||
|
||||
|
||||
filled := int(percent / 100.0 * float64(width))
|
||||
result := ""
|
||||
|
||||
|
||||
for i := 0; i < width; i++ {
|
||||
if i < filled {
|
||||
result += "█"
|
||||
@@ -399,7 +443,7 @@ func Meter(percent float64, width int) string {
|
||||
result += "░"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -415,7 +459,11 @@ func Bytes(bytes uint64) string {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
return fmt.Sprintf(
|
||||
"%.1f %cB",
|
||||
float64(bytes)/float64(div),
|
||||
"KMGTPE"[exp],
|
||||
)
|
||||
}
|
||||
|
||||
// Heat returns a color between blue and red based on the value.
|
||||
@@ -427,16 +475,16 @@ func Heat(value float64) color.Color {
|
||||
if value > 1 {
|
||||
value = 1
|
||||
}
|
||||
|
||||
|
||||
// Simple linear interpolation between blue and red
|
||||
r := uint8(255 * value)
|
||||
g := uint8(0)
|
||||
b := uint8(255 * (1 - value))
|
||||
|
||||
|
||||
return color.RGBA{r, g, b, 255}
|
||||
}
|
||||
|
||||
// RGB creates a color from red, green, and blue values (0-255).
|
||||
func RGB(r, g, b uint8) color.Color {
|
||||
return color.RGBA{r, g, b, 255}
|
||||
}
|
||||
}
|
||||
|
||||
81
internal/layout/draw_test.go
Normal file
81
internal/layout/draw_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package layout
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.eeqj.de/sneak/hdmistat/internal/fbdraw"
|
||||
)
|
||||
|
||||
func TestBasicDrawing(t *testing.T) {
|
||||
// Create a small grid for testing
|
||||
grid := fbdraw.NewCharGrid(40, 10)
|
||||
draw := NewDraw(grid)
|
||||
|
||||
// Clear the screen
|
||||
draw.Clear()
|
||||
|
||||
// Draw some text
|
||||
draw.Color(Color("white")).Text(5, 2, "Hello")
|
||||
draw.TextCenter(0, 4, "Centered")
|
||||
|
||||
// Create a sub-grid with border
|
||||
subGrid := draw.Grid(1, 1, 38, 8)
|
||||
subGrid.Border(Color("gray50"))
|
||||
|
||||
// Basic checks
|
||||
if grid.Width != 40 {
|
||||
t.Errorf("Expected width 40, got %d", grid.Width)
|
||||
}
|
||||
if grid.Height != 10 {
|
||||
t.Errorf("Expected height 10, got %d", grid.Height)
|
||||
}
|
||||
|
||||
// Check that some text was written (not all cells are empty)
|
||||
hasContent := false
|
||||
for y := 0; y < grid.Height; y++ {
|
||||
for x := 0; x < grid.Width; x++ {
|
||||
if grid.Cells[y][x].Rune != ' ' {
|
||||
hasContent = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasContent {
|
||||
t.Error("Expected some content in the grid, but all cells are empty")
|
||||
}
|
||||
|
||||
// Print the grid for visual inspection
|
||||
t.Logf("Rendered grid:\n%s", grid)
|
||||
}
|
||||
|
||||
func TestHelloWorldScenario(t *testing.T) {
|
||||
// Simulate the hello world scenario
|
||||
grid := fbdraw.NewCharGrid(80, 25)
|
||||
draw := NewDraw(grid)
|
||||
|
||||
draw.Clear()
|
||||
|
||||
centerY := grid.Height / 2
|
||||
|
||||
draw.Color(Color("cyan")).Bold()
|
||||
draw.TextCenter(0, centerY-2, "Hello World")
|
||||
|
||||
draw.Color(Color("white")).Plain()
|
||||
draw.TextCenter(0, centerY, "12:34:56")
|
||||
|
||||
draw.Color(Color("gray60"))
|
||||
draw.TextCenter(0, centerY+2, "Uptime: 1:23")
|
||||
|
||||
borderGrid := draw.Grid(2, 2, grid.Width-4, grid.Height-4)
|
||||
borderGrid.Border(Color("gray30"))
|
||||
|
||||
// Check that the grid has the expected content
|
||||
gridStr := grid.String()
|
||||
t.Logf("Hello World grid:\n%s", gridStr)
|
||||
|
||||
// Very basic check - just ensure it's not empty
|
||||
if len(gridStr) == 0 {
|
||||
t.Error("Grid string is empty")
|
||||
}
|
||||
}
|
||||
@@ -10,37 +10,37 @@ import (
|
||||
|
||||
// ExampleScreen shows how to create a screen that implements FrameGenerator
|
||||
type ExampleScreen struct {
|
||||
name string
|
||||
fps float64
|
||||
name string
|
||||
fps float64
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func (s *ExampleScreen) GenerateFrame(grid *fbdraw.CharGrid) error {
|
||||
// Create a draw context with the grid dimensions
|
||||
draw := layout.NewDraw(grid.Width, grid.Height)
|
||||
|
||||
// Create a draw context that works on the provided grid
|
||||
draw := layout.NewDraw(grid)
|
||||
|
||||
// Clear the screen
|
||||
draw.Clear()
|
||||
|
||||
|
||||
// Draw a title
|
||||
draw.Color(layout.Color("cyan")).Size(16).Bold()
|
||||
draw.Color(layout.Color("cyan")).Bold()
|
||||
draw.TextCenter(0, 2, "Example Screen: %s", s.name)
|
||||
|
||||
|
||||
// Create a grid for structured layout
|
||||
contentGrid := draw.Grid(5, 5, 70, 20)
|
||||
contentGrid.Border(layout.Color("gray50"))
|
||||
|
||||
|
||||
// Add some content
|
||||
contentGrid.Color(layout.Color("white")).WriteCenter(1, "Current Time: %s", time.Now().Format("15:04:05"))
|
||||
|
||||
|
||||
// Draw a progress bar
|
||||
contentGrid.Color(layout.Color("green")).Bar(10, 5, 50, 75.0, layout.Color("green"))
|
||||
|
||||
|
||||
// Add system stats
|
||||
contentGrid.Color(layout.Color("yellow")).Write(2, 8, "CPU: %.1f%%", 42.5)
|
||||
contentGrid.Color(layout.Color("orange")).Write(2, 9, "Memory: %s / %s", layout.Bytes(4*1024*1024*1024), layout.Bytes(16*1024*1024*1024))
|
||||
|
||||
// Return the rendered grid
|
||||
*grid = *draw.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -48,17 +48,23 @@ func (s *ExampleScreen) FramesPerSecond() float64 {
|
||||
return s.fps
|
||||
}
|
||||
|
||||
func (s *ExampleScreen) Init(width, height int) error {
|
||||
s.width = width
|
||||
s.height = height
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestExampleUsage(t *testing.T) {
|
||||
// Create carousel with terminal display for testing
|
||||
display := fbdraw.NewTerminalDisplay(80, 25)
|
||||
carousel := fbdraw.NewCarousel(display, 5*time.Second)
|
||||
|
||||
// This is just an example - in real usage you'd use NewFBDisplayAuto()
|
||||
// For testing we'll skip since we don't have a framebuffer
|
||||
t.Skip("Example test - requires framebuffer")
|
||||
|
||||
// Add screens
|
||||
carousel.AddScreen(&ExampleScreen{name: "Dashboard", fps: 1.0})
|
||||
carousel.AddScreen(&ExampleScreen{name: "System Monitor", fps: 2.0})
|
||||
carousel.AddScreen(&ExampleScreen{name: "Network Stats", fps: 0.5})
|
||||
|
||||
_ = carousel.AddScreen("Dashboard", &ExampleScreen{name: "Dashboard", fps: 1.0})
|
||||
_ = carousel.AddScreen("System Monitor", &ExampleScreen{name: "System Monitor", fps: 2.0})
|
||||
_ = carousel.AddScreen("Network Stats", &ExampleScreen{name: "Network Stats", fps: 0.5})
|
||||
|
||||
// In a real application, you would run this in a goroutine
|
||||
// ctx := context.Background()
|
||||
// go carousel.Run(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user