// Package layout provides a simple API for creating text-based layouts // that can be rendered to fbdraw grids for display in a carousel. package layout import ( "fmt" "image/color" "git.eeqj.de/sneak/hdmistat/internal/fbdraw" "git.eeqj.de/sneak/hdmistat/internal/font" ) // Font represents a bundled monospace font. type Font int const ( // PlexMono is IBM Plex Mono, a modern monospace font with good readability. PlexMono Font = iota // Terminus is a bitmap font optimized for long-term reading on terminals. Terminus // SourceCodePro is Adobe's Source Code Pro, designed for coding environments. SourceCodePro ) // Color returns a standard color by name func Color(name string) color.Color { switch name { case "black": return color.RGBA{0, 0, 0, 255} case "white": return color.RGBA{255, 255, 255, 255} case "red": return color.RGBA{255, 0, 0, 255} case "green": return color.RGBA{0, 255, 0, 255} case "blue": return color.RGBA{0, 0, 255, 255} case "yellow": return color.RGBA{255, 255, 0, 255} case "cyan": return color.RGBA{0, 255, 255, 255} case "magenta": return color.RGBA{255, 0, 255, 255} case "orange": return color.RGBA{255, 165, 0, 255} case "purple": return color.RGBA{128, 0, 128, 255} case "gray10": return color.RGBA{26, 26, 26, 255} case "gray20": return color.RGBA{51, 51, 51, 255} case "gray30": return color.RGBA{77, 77, 77, 255} case "gray40": return color.RGBA{102, 102, 102, 255} case "gray50": return color.RGBA{128, 128, 128, 255} case "gray60": return color.RGBA{153, 153, 153, 255} case "gray70": return color.RGBA{179, 179, 179, 255} case "gray80": return color.RGBA{204, 204, 204, 255} case "gray90": return color.RGBA{230, 230, 230, 255} default: return color.RGBA{255, 255, 255, 255} // Default to white } } // Draw provides the drawing context for creating a layout. // It maintains state for font, size, colors, and text styling. type Draw struct { // Drawing state font Font fontSize int bold bool italic bool fgColor color.Color bgColor color.Color // Grid to render to grid *fbdraw.CharGrid // Cached dimensions Width int 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}, } } // Render returns the current grid for rendering by the carousel func (d *Draw) Render() *fbdraw.CharGrid { return d.grid } // Clear fills the entire display with black. func (d *Draw) Clear() { d.grid.Clear(color.RGBA{0, 0, 0, 255}) } // ClearColor fills the entire display with the specified color. func (d *Draw) ClearColor(c color.Color) { // Fill all cells with spaces and the background color for y := 0; y < d.Height; y++ { for x := 0; x < d.Width; x++ { weight := font.WeightRegular if d.bold { weight = font.WeightBold } d.grid.SetCell(x, y, ' ', d.fgColor, c, weight, d.italic) } } } // Font sets the current font for text operations. func (d *Draw) Font(f Font) *Draw { d.font = f 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 return d } // Plain disables bold and italic text rendering. func (d *Draw) Plain() *Draw { d.bold = false d.italic = false return d } // Italic enables italic text rendering. func (d *Draw) Italic() *Draw { d.italic = true return d } // Color sets the foreground color for text operations. func (d *Draw) Color(c color.Color) *Draw { d.fgColor = c return d } // Background sets the background color for text operations. func (d *Draw) Background(c color.Color) *Draw { d.bgColor = c return d } // Text draws text at the specified character coordinates. func (d *Draw) Text(x, y int, format string, args ...interface{}) { text := fmt.Sprintf(format, args...) writer := fbdraw.NewGridWriter(d.grid) writer.MoveAbs(x, y) writer.SetColor(d.fgColor) writer.SetBackground(d.bgColor) if d.bold { writer.SetWeight(font.WeightBold) } else { writer.SetWeight(font.WeightRegular) } writer.SetItalic(d.italic) writer.Write(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) } // Grid creates a text grid region for simplified text layout. // The grid uses the current font settings from the Draw context. func (d *Draw) Grid(x, y, cols, rows int) *Grid { return &Grid{ draw: d, x: x, y: y, cols: cols, rows: rows, } } // Grid represents a rectangular text grid for structured text layout. // All positions are in character cells, not pixels. type Grid struct { draw *Draw x, y int cols, rows int // Grid-specific state that can override draw state fgColor color.Color bgColor color.Color borderColor color.Color hasBorder bool } // 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{}) { if row < 0 || row >= g.rows || col < 0 || col >= g.cols { 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) if g.fgColor != nil { writer.SetColor(g.fgColor) } else { writer.SetColor(g.draw.fgColor) } if g.bgColor != nil { writer.SetBackground(g.bgColor) } 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) } // WriteCenter centers text within the specified row. func (g *Grid) WriteCenter(row int, format string, args ...interface{}) { if row < 0 || row >= g.rows { return } text := fmt.Sprintf(format, args...) col := (g.cols - len(text)) / 2 if col < 0 { col = 0 } g.Write(col, row, text) } // Color sets the foreground color for subsequent Write operations. func (g *Grid) Color(c color.Color) *Grid { g.fgColor = c return g } // Background sets the background color for the entire grid. func (g *Grid) Background(c color.Color) *Grid { g.bgColor = c // Fill the grid area with the background color for row := 0; row < g.rows; row++ { for col := 0; col < g.cols; col++ { weight := font.WeightRegular 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) } } return g } // Border draws a border around the grid in the specified color. func (g *Grid) Border(c color.Color) *Grid { g.borderColor = c g.hasBorder = true // 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("┌") for i := 0; i < g.cols; i++ { writer.Write("─") } writer.Write("┐") // Side borders for row := 0; row < g.rows; row++ { writer.MoveAbs(g.x-1, g.y+row) writer.Write("│") writer.MoveAbs(g.x+g.cols, g.y+row) writer.Write("│") } // Bottom border writer.MoveAbs(g.x-1, g.y+g.rows) writer.Write("└") for i := 0; i < g.cols; i++ { writer.Write("─") } writer.Write("┘") return g } // RowBackground sets the background color for a specific row. func (g *Grid) RowBackground(row int, c color.Color) { if row < 0 || row >= g.rows { 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) } } // RowColor sets the text color for an entire row. func (g *Grid) RowColor(row int, c color.Color) { if row < 0 || row >= g.rows { return } // This would need to track row colors for subsequent writes // For now, we'll just update existing text in the row for col := 0; col < g.cols; col++ { // Get current cell to preserve other attributes if g.y+row < g.draw.grid.Height && g.x+col < g.draw.grid.Width { cell := &g.draw.grid.Cells[g.y+row][g.x+col] cell.Foreground = c } } } // Bar draws a horizontal progress bar within the grid cell. 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) writer.DrawMeter(percent, width) } // Bold enables bold text for subsequent Write operations. func (g *Grid) Bold() *Grid { // TODO: Track bold state for grid return g } // Plain disables text styling for subsequent Write operations. func (g *Grid) Plain() *Grid { // TODO: Clear text styling for grid return g } // Meter creates a text-based progress meter using Unicode block characters. // The width parameter specifies the number of characters. // Returns a string like "█████████░░░░░░" for 60% with width 15. func Meter(percent float64, width int) string { if percent < 0 { percent = 0 } if percent > 100 { percent = 100 } filled := int(percent / 100.0 * float64(width)) result := "" for i := 0; i < width; i++ { if i < filled { result += "█" } else { result += "░" } } return result } // Bytes formats a byte count as a human-readable string. // For example: 1234567890 becomes "1.2 GB". func Bytes(bytes uint64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := uint64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) } // Heat returns a color between blue and red based on the value. // 0.0 returns blue, 1.0 returns red, with gradients in between. func Heat(value float64) color.Color { if value < 0 { value = 0 } 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} }