package tui import ( "github.com/lixenwraith/color" "github.com/lixenwraith/terminal" ) // OverlayStyle specifies overlay appearance type OverlayStyle uint8 const ( OverlayFullscreen OverlayStyle = iota // No border, fills region OverlayModal // Centered box with border OverlayFloating // Positioned box with shadow OverlayBorderTitle // Title embedded in top border line ) // OverlayOpts configures overlay rendering type OverlayOpts struct { Style OverlayStyle Title string Border LineType Bg color.RGB Fg color.RGB // Border and title color TitleBg color.RGB // Title bar background, zero = same as Fg TitleFg color.RGB // Title text color, zero = same as Bg // Modal/Floating positioning (ignored for Fullscreen) Width int // 0 = 80% of region Height int // 0 = 80% of region X, Y int // Offset from center, 0 = centered // Shadow for Floating style ShadowColor color.RGB } // DefaultOverlayOpts returns sensible defaults for modal overlay func DefaultOverlayOpts(title string) OverlayOpts { return OverlayOpts{ Style: OverlayModal, Title: title, Border: LineDouble, Bg: color.RGB{R: 25, G: 25, B: 35}, Fg: color.RGB{R: 100, G: 140, B: 180}, TitleBg: color.RGB{R: 40, G: 60, B: 90}, TitleFg: color.RGB{R: 255, G: 255, B: 255}, } } // FullscreenOverlayOpts returns opts for fullscreen overlay with title bar func FullscreenOverlayOpts(title string) OverlayOpts { return OverlayOpts{ Style: OverlayFullscreen, Title: title, Bg: color.RGB{R: 20, G: 20, B: 30}, Fg: color.RGB{R: 100, G: 140, B: 180}, TitleBg: color.RGB{R: 40, G: 60, B: 90}, TitleFg: color.RGB{R: 255, G: 255, B: 255}, } } // OverlayResult contains regions returned by Overlay rendering type OverlayResult struct { Outer Region // Full overlay bounds (including border/title) Content Region // Inner content area TitleY int // Y position of title bar in Outer, -1 if no title } // Overlay renders an overlay and returns content region for caller to populate // Caller should render content into result.Content after this call func (r Region) Overlay(opts OverlayOpts) OverlayResult { if r.W < 3 || r.H < 3 { return OverlayResult{} } switch opts.Style { case OverlayFullscreen: return r.renderFullscreenOverlay(opts) case OverlayModal: return r.renderModalOverlay(opts) case OverlayFloating: return r.renderFloatingOverlay(opts) case OverlayBorderTitle: return r.renderBorderTitleOverlay(opts) } return OverlayResult{} } func (r Region) renderFullscreenOverlay(opts OverlayOpts) OverlayResult { r.Fill(opts.Bg) result := OverlayResult{ Outer: r, TitleY: -1, } contentY := 0 contentH := r.H // Title bar if opts.Title != "" { titleBg := opts.TitleBg if titleBg == (color.RGB{}) { titleBg = opts.Fg } titleFg := opts.TitleFg if titleFg == (color.RGB{}) { titleFg = opts.Bg } // Fill title row for x := 0; x < r.W; x++ { r.Cell(x, 0, ' ', titleFg, titleBg, terminal.AttrNone) } // Center title text title := opts.Title if RuneLen(title) > r.W-4 { title = Truncate(title, r.W-4) } titleX := (r.W - RuneLen(title)) / 2 r.Text(titleX, 0, title, titleFg, titleBg, terminal.AttrBold) result.TitleY = 0 contentY = 1 contentH = r.H - 1 } result.Content = r.Sub(0, contentY, r.W, contentH) return result } func (r Region) renderModalOverlay(opts OverlayOpts) OverlayResult { // Calculate dimensions w := opts.Width if w <= 0 { w = r.W * 80 / 100 } h := opts.Height if h <= 0 { h = r.H * 80 / 100 } // Clamp to region if w > r.W-2 { w = r.W - 2 } if h > r.H-2 { h = r.H - 2 } if w < 5 { w = 5 } if h < 3 { h = 3 } // Center with offset x := (r.W-w)/2 + opts.X y := (r.H-h)/2 + opts.Y // Clamp position if x < 0 { x = 0 } if y < 0 { y = 0 } if x+w > r.W { x = r.W - w } if y+h > r.H { y = r.H - h } outer := r.Sub(x, y, w, h) outer.BoxFilled(opts.Border, opts.Fg, opts.Bg) result := OverlayResult{ Outer: outer, TitleY: -1, } // Content inset by border contentX := 1 contentY := 1 contentW := w - 2 contentH := h - 2 // Title in top border if opts.Title != "" && contentW > 2 { titleBg := opts.TitleBg if titleBg == (color.RGB{}) { titleBg = opts.Fg } titleFg := opts.TitleFg if titleFg == (color.RGB{}) { titleFg = opts.Bg } // Fill title row inside border for i := 0; i < contentW; i++ { outer.Cell(contentX+i, contentY, ' ', titleFg, titleBg, terminal.AttrNone) } title := opts.Title if RuneLen(title) > contentW-2 { title = Truncate(title, contentW-2) } titleX := contentX + (contentW-RuneLen(title))/2 outer.Text(titleX, contentY, title, titleFg, titleBg, terminal.AttrBold) result.TitleY = contentY contentY++ contentH-- } if contentH < 1 { contentH = 1 } result.Content = outer.Sub(contentX, contentY, contentW, contentH) return result } func (r Region) renderFloatingOverlay(opts OverlayOpts) OverlayResult { // Same as modal but with shadow shadowColor := opts.ShadowColor if shadowColor == (color.RGB{}) { shadowColor = color.RGB{R: 10, G: 10, B: 15} } // Calculate dimensions (same as modal) w := opts.Width if w <= 0 { w = r.W * 80 / 100 } h := opts.Height if h <= 0 { h = r.H * 80 / 100 } if w > r.W-3 { w = r.W - 3 } if h > r.H-2 { h = r.H - 2 } if w < 5 { w = 5 } if h < 3 { h = 3 } x := (r.W-w)/2 + opts.X - 1 // Offset for shadow y := (r.H-h)/2 + opts.Y if x < 0 { x = 0 } if y < 0 { y = 0 } if x+w+1 > r.W { x = r.W - w - 1 } if y+h+1 > r.H { y = r.H - h - 1 } // Draw shadow shadowR := r.Sub(x+1, y+1, w, h) shadowR.Fill(shadowColor) // Draw box over shadow outer := r.Sub(x, y, w, h) outer.BoxFilled(opts.Border, opts.Fg, opts.Bg) result := OverlayResult{ Outer: outer, TitleY: -1, } contentX := 1 contentY := 1 contentW := w - 2 contentH := h - 2 if opts.Title != "" && contentW > 2 { titleBg := opts.TitleBg if titleBg == (color.RGB{}) { titleBg = opts.Fg } titleFg := opts.TitleFg if titleFg == (color.RGB{}) { titleFg = opts.Bg } for i := 0; i < contentW; i++ { outer.Cell(contentX+i, contentY, ' ', titleFg, titleBg, terminal.AttrNone) } title := opts.Title if RuneLen(title) > contentW-2 { title = Truncate(title, contentW-2) } titleX := contentX + (contentW-RuneLen(title))/2 outer.Text(titleX, contentY, title, titleFg, titleBg, terminal.AttrBold) result.TitleY = contentY contentY++ contentH-- } if contentH < 1 { contentH = 1 } result.Content = outer.Sub(contentX, contentY, contentW, contentH) return result } func (r Region) renderBorderTitleOverlay(opts OverlayOpts) OverlayResult { r.BoxFilled(opts.Border, opts.Fg, opts.Bg) result := OverlayResult{ Outer: r, TitleY: 0, } if opts.Title != "" && r.W > 6 { titleFg := opts.TitleFg if titleFg == (color.RGB{}) { titleFg = opts.Fg } title := " " + opts.Title + " " if RuneLen(title) > r.W-4 { title = Truncate(title, r.W-4) } titleX := (r.W - RuneLen(title)) / 2 r.Text(titleX, 0, title, titleFg, opts.Bg, terminal.AttrBold) } result.Content = r.Sub(1, 1, r.W-2, r.H-2) return result } // OverlayState manages overlay visibility and content state type OverlayState struct { Visible bool Opts OverlayOpts Data any // Application-specific state } // NewOverlayState creates hidden overlay state func NewOverlayState(opts OverlayOpts) *OverlayState { return &OverlayState{ Visible: false, Opts: opts, } } // Show makes overlay visible func (o *OverlayState) Show() { o.Visible = true } // Hide makes overlay invisible func (o *OverlayState) Hide() { o.Visible = false } // Toggle switches visibility func (o *OverlayState) Toggle() { o.Visible = !o.Visible }