package render import ( "fmt" "io" "strings" ) type ( trimDir uint8 overlapType uint8 ) const ( TrimLeft trimDir = iota TrimRight CoverNone overlapType = iota CoverTotal CoverWithin CoverLeft CoverRight ) type Instruction interface { // Write writes the instruction to the given io.Writer. Write(io.Writer) // Pos returns the x,y position of the instruction. Pos() (int, int) // Len returns the length of the instructions content. Len() int // Trim removes n characters FROM one side of the instruction. Trim(n int, d trimDir) Instruction // Copy creates a new instruction with the same content and position. Copy() Instruction } func overlap(over, under Instruction) overlapType { if over == nil || under == nil { return CoverNone } ox, oy := over.Pos() ux, uy := under.Pos() os, us := over.Len(), under.Len() // wrong row: if oy != uy { return CoverNone } // totally covers it if ox <= ux && ox+os >= ux+us { return CoverTotal } // Overlap on the left: if ox <= ux && ox+os > ux { return CoverLeft } // Overlap on the right: if ox < ux+us && ox+os >= ux+us { return CoverRight } // centered inside: if ox > ux && ox+os < ux+us { return CoverWithin } // Only other option is not touching return CoverNone } // Squash creates new instruction from overlapping instructions. // Whenever an instruction would be written over another instruction, // the one under is trimmed to only include the part that is not covered. // Three cases are possible: // - The under instruction is completely covered by the over instruction. // - The under instruction is partially covered on one side // - The under instruction is covered in the center (splitting it in two) // No matter what, the over instruction never changes so only variations of // under needs to be returned. func squash(over Instruction, under Instruction, ot overlapType) []Instruction { if over == nil || under == nil { return nil } ox, _ := over.Pos() ux, _ := under.Pos() os, us := over.Len(), under.Len() switch ot { case CoverTotal: return []Instruction{} case CoverLeft: overlap := ox + os - ux under = under.Trim(overlap, TrimLeft) return []Instruction{under} case CoverRight: overlap := ux + us - ox under = under.Trim(overlap, TrimRight) return []Instruction{under} case CoverWithin: overlapR := ox - ux + os overlapL := ux + us - ox right := under.Copy().Trim(overlapR, TrimLeft) left := under.Trim(overlapL, TrimRight) return []Instruction{left, right} default: return []Instruction{under} } } func reach(i Instruction) string { x, y := i.Pos() return fmt.Sprintf("\033[%d;%dH", y, x) } // DrawInstruction is a struct that contains content the renderer // should draw. type DrawInstruction struct { // Content is the string that should be drawn. Content string // Pos is the position in the terminal where the content should be drawn. X int Y int // Decorators is a list of ANSI escape codes used to decorate the content. Decorators []string } func (d DrawInstruction) Pos() (int, int) { return d.X, d.Y } func (d DrawInstruction) Len() int { return len(d.Content) } func (d DrawInstruction) Write(w io.Writer) { msg := reach(d) + strings.Join(d.Decorators, "") + d.Content io.WriteString(w, msg) } func (d DrawInstruction) Trim(n int, from trimDir) Instruction { // Prevents having to error check for negative values. if n < 0 { n = -n } // If n is greater than the length of the content, return nil. if n > len(d.Content) { return nil } switch from { case TrimLeft: d.Content = d.Content[n:] d.X += n case TrimRight: d.Content = d.Content[:len(d.Content)-n] } return d } func (d DrawInstruction) Copy() Instruction { return &DrawInstruction{ Content: d.Content, X: d.X, Y: d.Y, Decorators: d.Decorators, } } // ClearInstruction is a struct that contains content the renderer should // clear. they usually accompany a DrawInstruction (i.e. clear the space // behind a moving object) but not always. type ClearInstruction struct { // Len is the number of consecutive characters to clear. Size int // Pos is the position in the terminal where the content should be cleared. X int Y int } func (c ClearInstruction) Write(w io.Writer) { // Erase as many characters as the size of the instruction. msg := reach(c) + "\033[0m" + strings.Repeat(" ", c.Size) // TODO: maybe implement a way to log stuff like writestring errors io.WriteString(w, msg) } func (c ClearInstruction) Pos() (int, int) { return c.X, c.Y } func (c ClearInstruction) Len() int { if c.Size < 0 { return -c.Size } return c.Size } func (c ClearInstruction) Trim(n int, from trimDir) Instruction { // Prevents having to error check for negative values. if n < 0 { n = -n } // If n is greater than the length of the content, return nil. if n > c.Size { return nil } switch from { case TrimLeft: c.Size -= n c.X += n case TrimRight: c.Size -= n } return c } func (c ClearInstruction) Copy() Instruction { return &ClearInstruction{ Size: c.Size, X: c.X, Y: c.Y, } }