about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBen Burwell <ben@benburwell.com>2019-12-20 13:21:32 -0500
committerDrew DeVault <sir@cmpwn.com>2019-12-21 09:23:20 -0500
commitbcd03c4c4a94e73b2545bf5dfc404082a674c76e (patch)
tree8366af84dfd9011d20794f2a7556e7d6fc71b7db
parentef4c2f61d8c599ac25b4f465ffa3bea9ca6ce1a3 (diff)
downloadaerc-bcd03c4c4a94e73b2545bf5dfc404082a674c76e.tar.gz
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.
-rw-r--r--lib/ui/context.go23
-rw-r--r--lib/ui/popover.go62
-rw-r--r--lib/ui/ui.go26
3 files changed, 102 insertions, 9 deletions
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
 	}