200 lines
4.6 KiB
Go
200 lines
4.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"github.com/lixenwraith/color"
|
|
"github.com/lixenwraith/terminal"
|
|
)
|
|
|
|
// EditorOpts configures editor rendering
|
|
type EditorOpts struct {
|
|
LineNumbers bool
|
|
LineNumWidth int // 0 = auto-size
|
|
WrapLines bool
|
|
Border LineType
|
|
Focused bool
|
|
Style EditorStyle
|
|
}
|
|
|
|
// EditorStyle defines editor colors
|
|
type EditorStyle struct {
|
|
TextFg color.RGB
|
|
TextBg color.RGB
|
|
CursorFg color.RGB
|
|
CursorBg color.RGB
|
|
LineNumFg color.RGB
|
|
LineNumBg color.RGB
|
|
CurrentLineBg color.RGB
|
|
BorderFg color.RGB
|
|
}
|
|
|
|
// DefaultEditorStyle returns default colors
|
|
func DefaultEditorStyle() EditorStyle {
|
|
return EditorStyle{
|
|
TextFg: color.RGB{R: 220, G: 220, B: 220},
|
|
TextBg: color.RGB{R: 25, G: 25, B: 35},
|
|
CursorFg: color.RGB{R: 0, G: 0, B: 0},
|
|
CursorBg: color.RGB{R: 200, G: 200, B: 200},
|
|
LineNumFg: color.RGB{R: 100, G: 100, B: 120},
|
|
LineNumBg: color.RGB{R: 30, G: 30, B: 40},
|
|
CurrentLineBg: color.RGB{R: 35, G: 35, B: 50},
|
|
BorderFg: color.RGB{R: 80, G: 80, B: 100},
|
|
}
|
|
}
|
|
|
|
// Editor renders multi-line editor and returns content height used
|
|
func (r Region) Editor(state *EditorState, opts EditorOpts) int {
|
|
if r.W < 3 || r.H < 1 {
|
|
return 0
|
|
}
|
|
|
|
style := opts.Style
|
|
if style == (EditorStyle{}) {
|
|
style = DefaultEditorStyle()
|
|
}
|
|
|
|
// Calculate content area accounting for border
|
|
contentX := 0
|
|
contentY := 0
|
|
contentW := r.W
|
|
contentH := r.H
|
|
|
|
if opts.Border != LineNone {
|
|
if r.H < 3 {
|
|
return 0
|
|
}
|
|
r.Box(opts.Border, style.BorderFg)
|
|
contentX = 1
|
|
contentY = 1
|
|
contentW = r.W - 2
|
|
contentH = r.H - 2
|
|
}
|
|
|
|
// Line number gutter
|
|
gutterW := 0
|
|
if opts.LineNumbers {
|
|
gutterW = opts.LineNumWidth
|
|
if gutterW == 0 {
|
|
digits := 1
|
|
n := len(state.Lines)
|
|
for n >= 10 {
|
|
digits++
|
|
n /= 10
|
|
}
|
|
gutterW = digits + 1
|
|
}
|
|
contentW -= gutterW
|
|
}
|
|
|
|
if contentW < 1 || contentH < 1 {
|
|
return 0
|
|
}
|
|
|
|
state.AdjustScroll(contentW, contentH)
|
|
|
|
// Render each visible line
|
|
for y := 0; y < contentH; y++ {
|
|
lineIdx := state.ScrollY + y
|
|
isCurrentLine := lineIdx == state.CursorLine
|
|
|
|
bg := style.TextBg
|
|
if isCurrentLine && opts.Focused {
|
|
bg = style.CurrentLineBg
|
|
}
|
|
|
|
// Line number gutter
|
|
if opts.LineNumbers {
|
|
for gx := 0; gx < gutterW-1; gx++ {
|
|
r.Cell(contentX+gx, contentY+y, ' ', style.LineNumFg, style.LineNumBg, terminal.AttrNone)
|
|
}
|
|
r.Cell(contentX+gutterW-1, contentY+y, '│', style.LineNumFg, style.LineNumBg, terminal.AttrDim)
|
|
|
|
if lineIdx < len(state.Lines) {
|
|
numStr := formatLineNum(lineIdx+1, gutterW-1)
|
|
for i, ch := range numStr {
|
|
r.Cell(contentX+i, contentY+y, ch, style.LineNumFg, style.LineNumBg, terminal.AttrNone)
|
|
}
|
|
}
|
|
}
|
|
|
|
textX := contentX + gutterW
|
|
|
|
// Fill text area with background
|
|
for x := 0; x < contentW; x++ {
|
|
r.Cell(textX+x, contentY+y, ' ', style.TextFg, bg, terminal.AttrNone)
|
|
}
|
|
|
|
if lineIdx >= len(state.Lines) {
|
|
continue
|
|
}
|
|
|
|
line := []rune(state.Lines[lineIdx])
|
|
|
|
// Scroll indicator left
|
|
if state.ScrollX > 0 && len(line) > 0 {
|
|
r.Cell(textX, contentY+y, '◀', style.LineNumFg, bg, terminal.AttrDim)
|
|
}
|
|
|
|
// Render visible text
|
|
for x := 0; x < contentW; x++ {
|
|
charIdx := state.ScrollX + x
|
|
if charIdx >= len(line) {
|
|
break
|
|
}
|
|
|
|
ch := line[charIdx]
|
|
fg := style.TextFg
|
|
cellBg := bg
|
|
|
|
if opts.Focused && lineIdx == state.CursorLine && charIdx == state.CursorCol {
|
|
fg = style.CursorFg
|
|
cellBg = style.CursorBg
|
|
}
|
|
|
|
r.Cell(textX+x, contentY+y, ch, fg, cellBg, terminal.AttrNone)
|
|
}
|
|
|
|
// Scroll indicator right
|
|
if state.ScrollX+contentW < len(line) {
|
|
r.Cell(textX+contentW-1, contentY+y, '▶', style.LineNumFg, bg, terminal.AttrDim)
|
|
}
|
|
|
|
// Cursor at end of line
|
|
if opts.Focused && lineIdx == state.CursorLine && state.CursorCol >= len(line) {
|
|
cursorX := state.CursorCol - state.ScrollX
|
|
if cursorX >= 0 && cursorX < contentW {
|
|
r.Cell(textX+cursorX, contentY+y, ' ', style.CursorFg, style.CursorBg, terminal.AttrNone)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Vertical scroll indicators
|
|
if state.ScrollY > 0 {
|
|
r.Cell(contentX+gutterW+contentW-1, contentY, '▲', style.LineNumFg, style.TextBg, terminal.AttrDim)
|
|
}
|
|
if state.ScrollY+contentH < len(state.Lines) {
|
|
r.Cell(contentX+gutterW+contentW-1, contentY+contentH-1, '▼', style.LineNumFg, style.TextBg, terminal.AttrDim)
|
|
}
|
|
|
|
if opts.Border != LineNone {
|
|
return r.H
|
|
}
|
|
return contentH
|
|
}
|
|
|
|
// formatLineNum formats line number right-aligned to width
|
|
func formatLineNum(num, width int) string {
|
|
s := ""
|
|
for num > 0 {
|
|
s = string(rune('0'+num%10)) + s
|
|
num /= 10
|
|
}
|
|
if s == "" {
|
|
s = "0"
|
|
}
|
|
for len(s) < width {
|
|
s = " " + s
|
|
}
|
|
return s
|
|
}
|
|
|