diff --git a/lib/ui/context.go b/lib/ui/context.go index d450fd8..6bdf76a 100644 --- a/lib/ui/context.go +++ b/lib/ui/context.go @@ -10,9 +10,10 @@ import ( // A context allows you to draw in a sub-region of the terminal type Context struct { - screen tcell.Screen - viewport *views.ViewPort - x, y int + screen tcell.Screen + viewport *views.ViewPort + x, y int + onPopover func(*Popover) } func (ctx *Context) X() int { @@ -35,9 +36,9 @@ func (ctx *Context) Height() int { return height } -func NewContext(width, height int, screen tcell.Screen) *Context { +func NewContext(width, height int, screen tcell.Screen, p func(*Popover)) *Context { vp := views.NewViewPort(screen, 0, 0, width, height) - return &Context{screen, vp, 0, 0} + return &Context{screen, vp, 0, 0, p} } func (ctx *Context) Subcontext(x, y, width, height int) *Context { @@ -49,7 +50,7 @@ func (ctx *Context) Subcontext(x, y, width, height int) *Context { panic(fmt.Errorf("Attempted to create context larger than parent")) } vp := views.NewViewPort(ctx.viewport, x, y, width, height) - return &Context{ctx.screen, vp, ctx.x + x, ctx.y + y} + return &Context{ctx.screen, vp, ctx.x + x, ctx.y + y, ctx.onPopover} } func (ctx *Context) SetCell(x, y int, ch rune, style tcell.Style) { @@ -113,3 +114,13 @@ func (ctx *Context) SetCursor(x, y int) { func (ctx *Context) HideCursor() { ctx.screen.HideCursor() } + +func (ctx *Context) Popover(x, y, width, height int, d Drawable) { + ctx.onPopover(&Popover{ + x: ctx.x + x, + y: ctx.y + y, + width: width, + height: height, + content: d, + }) +} diff --git a/lib/ui/popover.go b/lib/ui/popover.go new file mode 100644 index 0000000..a76f222 --- /dev/null +++ b/lib/ui/popover.go @@ -0,0 +1,62 @@ +package ui + +import "github.com/gdamore/tcell" + +type Popover struct { + x, y, width, height int + content Drawable +} + +func (p *Popover) Draw(ctx *Context) { + var subcontext *Context + + // trim desired width to fit + width := p.width + if p.x+p.width > ctx.Width() { + width = ctx.Width() - p.x + } + + if p.y+p.height+1 < ctx.Height() { + // draw below + subcontext = ctx.Subcontext(p.x, p.y+1, width, p.height) + } else if p.y-p.height >= 0 { + // draw above + subcontext = ctx.Subcontext(p.x, p.y-p.height, width, p.height) + } else { + // can't fit entirely above or below, so find the largest available + // vertical space and shrink to fit + if p.y > ctx.Height()-p.y { + // there is more space above than below + height := p.y + subcontext = ctx.Subcontext(p.x, 0, width, height) + } else { + // there is more space below than above + height := ctx.Height() - p.y + subcontext = ctx.Subcontext(p.x, p.y+1, width, height-1) + } + } + p.content.Draw(subcontext) +} + +func (p *Popover) Event(e tcell.Event) bool { + if di, ok := p.content.(DrawableInteractive); ok { + return di.Event(e) + } + return false +} + +func (p *Popover) Focus(f bool) { + if di, ok := p.content.(DrawableInteractive); ok { + di.Focus(f) + } +} + +func (p *Popover) Invalidate() { + p.content.Invalidate() +} + +func (p *Popover) OnInvalidate(f func(Drawable)) { + p.content.OnInvalidate(func(_ Drawable) { + f(p) + }) +} diff --git a/lib/ui/ui.go b/lib/ui/ui.go index 01d12dc..16b176d 100644 --- a/lib/ui/ui.go +++ b/lib/ui/ui.go @@ -11,6 +11,7 @@ type UI struct { exit atomic.Value // bool ctx *Context screen tcell.Screen + popover *Popover tcEvents chan tcell.Event invalid int32 // access via atomic @@ -34,11 +35,11 @@ func Initialize(content DrawableInteractiveBeeper) (*UI, error) { state := UI{ Content: content, - ctx: NewContext(width, height, screen), screen: screen, tcEvents: make(chan tcell.Event, 10), } + state.ctx = NewContext(width, height, screen, state.onPopover) state.exit.Store(false) go func() { @@ -57,6 +58,10 @@ func Initialize(content DrawableInteractiveBeeper) (*UI, error) { return &state, nil } +func (state *UI) onPopover(p *Popover) { + state.popover = p +} + func (state *UI) ShouldExit() bool { return state.exit.Load().(bool) } @@ -78,17 +83,32 @@ func (state *UI) Tick() bool { case *tcell.EventResize: state.screen.Clear() width, height := event.Size() - state.ctx = NewContext(width, height, state.screen) + state.ctx = NewContext(width, height, state.screen, state.onPopover) state.Content.Invalidate() } - state.Content.Event(event) + // if we have a popover, and it can handle the event, it does so + if state.popover == nil || !state.popover.Event(event) { + // otherwise, we send the event to the main content + state.Content.Event(event) + } more = true default: } wasInvalid := atomic.SwapInt32(&state.invalid, 0) if wasInvalid != 0 { + if state.popover != nil { + // if the previous frame had a popover, rerender the entire display + state.Content.Invalidate() + atomic.StoreInt32(&state.invalid, 0) + } + // reset popover for the next Draw + state.popover = nil state.Content.Draw(state.ctx) + if state.popover != nil { + // if the Draw resulted in a popover, draw it + state.popover.Draw(state.ctx) + } state.screen.Show() more = true }