summary refs log tree commit diff stats
diff options
context:
space:
mode:
-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
 	}