Add popovers

A popover is a special UI element which can be layered over the rest of
the UI (i.e. it is painted last) and can fall anywhere on the screen,
not just with the bounds of its parent's viewport/context. With these
special abilities comes the restriction that only one popover may be
visible on screen at once.

Popovers are requested from the UI context passed to Draw calls and
specify the anchor point and the desired dimensions. The popover is then
fit to the available space and placed relative to the anchor point.
This commit is contained in:
Ben Burwell 2019-12-20 13:21:32 -05:00 committed by Drew DeVault
parent ef4c2f61d8
commit bcd03c4c4a
3 changed files with 102 additions and 9 deletions

View file

@ -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,
})
}

62
lib/ui/popover.go Normal file
View file

@ -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)
})
}

View file

@ -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
}