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:
parent
ef4c2f61d8
commit
bcd03c4c4a
3 changed files with 102 additions and 9 deletions
|
@ -10,9 +10,10 @@ import (
|
||||||
|
|
||||||
// A context allows you to draw in a sub-region of the terminal
|
// A context allows you to draw in a sub-region of the terminal
|
||||||
type Context struct {
|
type Context struct {
|
||||||
screen tcell.Screen
|
screen tcell.Screen
|
||||||
viewport *views.ViewPort
|
viewport *views.ViewPort
|
||||||
x, y int
|
x, y int
|
||||||
|
onPopover func(*Popover)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *Context) X() int {
|
func (ctx *Context) X() int {
|
||||||
|
@ -35,9 +36,9 @@ func (ctx *Context) Height() int {
|
||||||
return height
|
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)
|
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 {
|
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"))
|
panic(fmt.Errorf("Attempted to create context larger than parent"))
|
||||||
}
|
}
|
||||||
vp := views.NewViewPort(ctx.viewport, x, y, width, height)
|
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) {
|
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() {
|
func (ctx *Context) HideCursor() {
|
||||||
ctx.screen.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
62
lib/ui/popover.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
26
lib/ui/ui.go
26
lib/ui/ui.go
|
@ -11,6 +11,7 @@ type UI struct {
|
||||||
exit atomic.Value // bool
|
exit atomic.Value // bool
|
||||||
ctx *Context
|
ctx *Context
|
||||||
screen tcell.Screen
|
screen tcell.Screen
|
||||||
|
popover *Popover
|
||||||
|
|
||||||
tcEvents chan tcell.Event
|
tcEvents chan tcell.Event
|
||||||
invalid int32 // access via atomic
|
invalid int32 // access via atomic
|
||||||
|
@ -34,11 +35,11 @@ func Initialize(content DrawableInteractiveBeeper) (*UI, error) {
|
||||||
|
|
||||||
state := UI{
|
state := UI{
|
||||||
Content: content,
|
Content: content,
|
||||||
ctx: NewContext(width, height, screen),
|
|
||||||
screen: screen,
|
screen: screen,
|
||||||
|
|
||||||
tcEvents: make(chan tcell.Event, 10),
|
tcEvents: make(chan tcell.Event, 10),
|
||||||
}
|
}
|
||||||
|
state.ctx = NewContext(width, height, screen, state.onPopover)
|
||||||
|
|
||||||
state.exit.Store(false)
|
state.exit.Store(false)
|
||||||
go func() {
|
go func() {
|
||||||
|
@ -57,6 +58,10 @@ func Initialize(content DrawableInteractiveBeeper) (*UI, error) {
|
||||||
return &state, nil
|
return &state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (state *UI) onPopover(p *Popover) {
|
||||||
|
state.popover = p
|
||||||
|
}
|
||||||
|
|
||||||
func (state *UI) ShouldExit() bool {
|
func (state *UI) ShouldExit() bool {
|
||||||
return state.exit.Load().(bool)
|
return state.exit.Load().(bool)
|
||||||
}
|
}
|
||||||
|
@ -78,17 +83,32 @@ func (state *UI) Tick() bool {
|
||||||
case *tcell.EventResize:
|
case *tcell.EventResize:
|
||||||
state.screen.Clear()
|
state.screen.Clear()
|
||||||
width, height := event.Size()
|
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.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
|
more = true
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
wasInvalid := atomic.SwapInt32(&state.invalid, 0)
|
wasInvalid := atomic.SwapInt32(&state.invalid, 0)
|
||||||
if wasInvalid != 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)
|
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()
|
state.screen.Show()
|
||||||
more = true
|
more = true
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue