v0.1.0 initial commit
This commit is contained in:
+301
@@ -0,0 +1,301 @@
|
||||
package tui
|
||||
|
||||
import "github.com/lixenwraith/terminal"
|
||||
|
||||
// ToastPosition specifies where toast renders
|
||||
type ToastPosition uint8
|
||||
|
||||
const (
|
||||
ToastBottom ToastPosition = iota // Full-width bar at bottom
|
||||
ToastTop // Full-width bar at top
|
||||
ToastBottomRight // Floating box bottom-right
|
||||
ToastBottomLeft // Floating box bottom-left
|
||||
ToastTopRight // Floating box top-right
|
||||
ToastTopLeft // Floating box top-left
|
||||
ToastCenter // Centered floating box
|
||||
)
|
||||
|
||||
// ToastSeverity defines message type for styling
|
||||
type ToastSeverity uint8
|
||||
|
||||
const (
|
||||
ToastInfo ToastSeverity = iota // Default, neutral
|
||||
ToastSuccess // Green, positive
|
||||
ToastWarning // Yellow, caution
|
||||
ToastError // Red, failure
|
||||
)
|
||||
|
||||
// ToastStyle defines visual appearance
|
||||
type ToastStyle uint8
|
||||
|
||||
const (
|
||||
ToastStyleMinimal ToastStyle = iota // No border, just text
|
||||
ToastStyleBar // Full-width background bar
|
||||
ToastStyleBox // Bordered box
|
||||
ToastStyleRounded // Rounded border box
|
||||
ToastStyleDouble // Double-line border
|
||||
ToastStyleShadow // Box with shadow effect
|
||||
)
|
||||
|
||||
// ToastIcons for severity levels
|
||||
var ToastIcons = map[ToastSeverity]rune{
|
||||
ToastInfo: 'ℹ',
|
||||
ToastSuccess: '✓',
|
||||
ToastWarning: '⚠',
|
||||
ToastError: '✗',
|
||||
}
|
||||
|
||||
// ToastColors default colors per severity
|
||||
var ToastColors = map[ToastSeverity]struct{ Fg, Bg, Icon terminal.RGB }{
|
||||
ToastInfo: {
|
||||
Fg: terminal.RGB{R: 200, G: 200, B: 200},
|
||||
Bg: terminal.RGB{R: 40, G: 40, B: 50},
|
||||
Icon: terminal.RGB{R: 100, G: 150, B: 255},
|
||||
},
|
||||
ToastSuccess: {
|
||||
Fg: terminal.RGB{R: 220, G: 255, B: 220},
|
||||
Bg: terminal.RGB{R: 30, G: 60, B: 30},
|
||||
Icon: terminal.RGB{R: 80, G: 220, B: 80},
|
||||
},
|
||||
ToastWarning: {
|
||||
Fg: terminal.RGB{R: 255, G: 240, B: 200},
|
||||
Bg: terminal.RGB{R: 60, G: 50, B: 20},
|
||||
Icon: terminal.RGB{R: 255, G: 200, B: 60},
|
||||
},
|
||||
ToastError: {
|
||||
Fg: terminal.RGB{R: 255, G: 220, B: 220},
|
||||
Bg: terminal.RGB{R: 60, G: 25, B: 25},
|
||||
Icon: terminal.RGB{R: 255, G: 80, B: 80},
|
||||
},
|
||||
}
|
||||
|
||||
// ToastOpts configures toast rendering
|
||||
type ToastOpts struct {
|
||||
Message string
|
||||
Severity ToastSeverity
|
||||
Position ToastPosition
|
||||
Style ToastStyle
|
||||
ShowIcon bool
|
||||
MinWidth int // Minimum width for floating toasts, 0 = auto
|
||||
MaxWidth int // Maximum width, 0 = region width
|
||||
Padding int // Horizontal padding, default 1
|
||||
MarginX int // Margin from edge for floating positions
|
||||
MarginY int // Margin from edge for floating positions
|
||||
CustomFg terminal.RGB
|
||||
CustomBg terminal.RGB
|
||||
CustomIcon terminal.RGB
|
||||
}
|
||||
|
||||
// DefaultToastOpts returns sensible defaults
|
||||
func DefaultToastOpts(message string, severity ToastSeverity) ToastOpts {
|
||||
return ToastOpts{
|
||||
Message: message,
|
||||
Severity: severity,
|
||||
Position: ToastBottom,
|
||||
Style: ToastStyleBar,
|
||||
ShowIcon: true,
|
||||
Padding: 1,
|
||||
MarginX: 2,
|
||||
MarginY: 1,
|
||||
}
|
||||
}
|
||||
|
||||
// Toast renders a toast message overlay
|
||||
// Returns the region occupied by the toast for hit testing
|
||||
func (r Region) Toast(opts ToastOpts) Region {
|
||||
if r.W < 5 || r.H < 1 || opts.Message == "" {
|
||||
return Region{}
|
||||
}
|
||||
|
||||
// Resolve colors
|
||||
fg, bg, iconFg := opts.CustomFg, opts.CustomBg, opts.CustomIcon
|
||||
if fg == (terminal.RGB{}) {
|
||||
fg = ToastColors[opts.Severity].Fg
|
||||
}
|
||||
if bg == (terminal.RGB{}) {
|
||||
bg = ToastColors[opts.Severity].Bg
|
||||
}
|
||||
if iconFg == (terminal.RGB{}) {
|
||||
iconFg = ToastColors[opts.Severity].Icon
|
||||
}
|
||||
|
||||
padding := opts.Padding
|
||||
if padding == 0 {
|
||||
padding = 1
|
||||
}
|
||||
|
||||
// Calculate content width
|
||||
iconW := 0
|
||||
if opts.ShowIcon {
|
||||
iconW = 2 // icon + space
|
||||
}
|
||||
msgLen := RuneLen(opts.Message)
|
||||
contentW := iconW + msgLen + padding*2
|
||||
|
||||
// Determine toast dimensions based on style
|
||||
borderW := 0
|
||||
if opts.Style >= ToastStyleBox {
|
||||
borderW = 2
|
||||
}
|
||||
|
||||
toastW := contentW + borderW
|
||||
toastH := 1 + borderW
|
||||
|
||||
// Apply width constraints
|
||||
maxW := opts.MaxWidth
|
||||
if maxW == 0 || maxW > r.W {
|
||||
maxW = r.W
|
||||
}
|
||||
if toastW > maxW {
|
||||
toastW = maxW
|
||||
}
|
||||
if opts.MinWidth > 0 && toastW < opts.MinWidth {
|
||||
toastW = opts.MinWidth
|
||||
}
|
||||
|
||||
// Calculate position
|
||||
var toastX, toastY int
|
||||
marginX := opts.MarginX
|
||||
marginY := opts.MarginY
|
||||
|
||||
switch opts.Position {
|
||||
case ToastBottom:
|
||||
toastX = 0
|
||||
toastY = r.H - toastH
|
||||
toastW = r.W // Full width for bar positions
|
||||
case ToastTop:
|
||||
toastX = 0
|
||||
toastY = 0
|
||||
toastW = r.W
|
||||
case ToastBottomRight:
|
||||
toastX = r.W - toastW - marginX
|
||||
toastY = r.H - toastH - marginY
|
||||
case ToastBottomLeft:
|
||||
toastX = marginX
|
||||
toastY = r.H - toastH - marginY
|
||||
case ToastTopRight:
|
||||
toastX = r.W - toastW - marginX
|
||||
toastY = marginY
|
||||
case ToastTopLeft:
|
||||
toastX = marginX
|
||||
toastY = marginY
|
||||
case ToastCenter:
|
||||
toastX = (r.W - toastW) / 2
|
||||
toastY = (r.H - toastH) / 2
|
||||
}
|
||||
|
||||
// Clamp position
|
||||
if toastX < 0 {
|
||||
toastX = 0
|
||||
}
|
||||
if toastY < 0 {
|
||||
toastY = 0
|
||||
}
|
||||
|
||||
toastRegion := r.Sub(toastX, toastY, toastW, toastH)
|
||||
|
||||
// Render based on style
|
||||
switch opts.Style {
|
||||
case ToastStyleMinimal:
|
||||
r.renderToastContent(toastRegion, opts, fg, bg, iconFg, 0)
|
||||
|
||||
case ToastStyleBar:
|
||||
toastRegion.Fill(bg)
|
||||
r.renderToastContent(toastRegion, opts, fg, bg, iconFg, 0)
|
||||
|
||||
case ToastStyleBox:
|
||||
toastRegion.BoxFilled(LineSingle, fg, bg)
|
||||
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
|
||||
|
||||
case ToastStyleRounded:
|
||||
toastRegion.BoxFilled(LineRounded, fg, bg)
|
||||
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
|
||||
|
||||
case ToastStyleDouble:
|
||||
toastRegion.BoxFilled(LineDouble, fg, bg)
|
||||
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
|
||||
|
||||
case ToastStyleShadow:
|
||||
// Shadow offset
|
||||
shadowRegion := r.Sub(toastX+1, toastY+1, toastW, toastH)
|
||||
shadowRegion.Fill(terminal.RGB{R: 10, G: 10, B: 15})
|
||||
toastRegion.BoxFilled(LineSingle, fg, bg)
|
||||
r.renderToastContent(toastRegion.Inset(1), opts, fg, bg, iconFg, 0)
|
||||
}
|
||||
|
||||
return toastRegion
|
||||
}
|
||||
|
||||
func (r Region) renderToastContent(content Region, opts ToastOpts, fg, bg, iconFg terminal.RGB, _ int) {
|
||||
if content.W < 1 || content.H < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
x := opts.Padding
|
||||
y := 0
|
||||
|
||||
// Icon
|
||||
if opts.ShowIcon {
|
||||
icon := ToastIcons[opts.Severity]
|
||||
if x < content.W {
|
||||
content.Cell(x, y, icon, iconFg, bg, terminal.AttrBold)
|
||||
}
|
||||
x += 2
|
||||
}
|
||||
|
||||
// Message
|
||||
msg := opts.Message
|
||||
availW := content.W - x - opts.Padding
|
||||
if availW < 1 {
|
||||
return
|
||||
}
|
||||
if RuneLen(msg) > availW {
|
||||
msg = Truncate(msg, availW)
|
||||
}
|
||||
content.Text(x, y, msg, fg, bg, terminal.AttrNone)
|
||||
}
|
||||
|
||||
// ToastState manages toast lifecycle
|
||||
type ToastState struct {
|
||||
Visible bool
|
||||
Opts ToastOpts
|
||||
FramesLeft int // Countdown to auto-dismiss, -1 = persistent
|
||||
}
|
||||
|
||||
// NewToastState creates a toast that auto-dismisses after frames
|
||||
// Use frames=-1 for persistent toast
|
||||
func NewToastState(opts ToastOpts, frames int) *ToastState {
|
||||
return &ToastState{
|
||||
Visible: true,
|
||||
Opts: opts,
|
||||
FramesLeft: frames,
|
||||
}
|
||||
}
|
||||
|
||||
// Tick decrements frame counter, returns true if toast should dismiss
|
||||
func (t *ToastState) Tick() bool {
|
||||
if !t.Visible {
|
||||
return false
|
||||
}
|
||||
if t.FramesLeft < 0 {
|
||||
return false // Persistent
|
||||
}
|
||||
t.FramesLeft--
|
||||
if t.FramesLeft <= 0 {
|
||||
t.Visible = false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Dismiss hides the toast
|
||||
func (t *ToastState) Dismiss() {
|
||||
t.Visible = false
|
||||
}
|
||||
|
||||
// Show displays a new toast
|
||||
func (t *ToastState) Show(opts ToastOpts, frames int) {
|
||||
t.Opts = opts
|
||||
t.FramesLeft = frames
|
||||
t.Visible = true
|
||||
}
|
||||
Reference in New Issue
Block a user