Polish up grid and add new rendering loop

This commit is contained in:
Drew DeVault 2018-02-17 16:35:36 -05:00
parent 1892d73161
commit 60b351b78c
8 changed files with 126 additions and 202 deletions

View File

@ -1,7 +1,6 @@
package main
import (
"fmt"
"io"
"io/ioutil"
"log"
@ -9,11 +8,30 @@ import (
"time"
"github.com/mattn/go-isatty"
tb "github.com/nsf/termbox-go"
"git.sr.ht/~sircmpwn/aerc2/config"
"git.sr.ht/~sircmpwn/aerc2/ui"
)
type fill rune
func (f fill) Draw(ctx *ui.Context) {
for x := 0; x < ctx.Width(); x += 1 {
for y := 0; y < ctx.Height(); y += 1 {
ctx.SetCell(x, y, rune(f), tb.ColorDefault, tb.ColorDefault)
}
}
}
func (f fill) OnInvalidate(callback func(d ui.Drawable)) {
// no-op
}
func (f fill) Invalidate() {
// no-op
}
func main() {
var logOut io.Writer
var logger *log.Logger
@ -29,20 +47,30 @@ func main() {
if err != nil {
panic(err)
}
_ui, err := ui.Initialize(conf)
grid := ui.NewGrid()
grid.Rows = []ui.DimSpec{
ui.DimSpec{ui.SIZE_EXACT, 4},
ui.DimSpec{ui.SIZE_WEIGHT, 1},
ui.DimSpec{ui.SIZE_WEIGHT, 1},
ui.DimSpec{ui.SIZE_EXACT, 1},
}
grid.Columns = []ui.DimSpec{
ui.DimSpec{ui.SIZE_WEIGHT, 3},
ui.DimSpec{ui.SIZE_WEIGHT, 2},
}
grid.AddChild(fill('★')).At(0, 0).Span(1, 2)
grid.AddChild(fill('☆')).At(1, 0).Span(1, 2)
grid.AddChild(fill('.')).At(2, 0).Span(1, 2)
grid.AddChild(fill('•')).At(2, 1).Span(1, 1)
grid.AddChild(fill('+')).At(3, 0).Span(1, 2)
_ui, err := ui.Initialize(conf, grid)
if err != nil {
panic(err)
}
defer _ui.Close()
for _, account := range conf.Accounts {
logger.Printf("Initializing account %s\n", account.Name)
tab, err := ui.NewAccountTab(&account, log.New(
logOut, fmt.Sprintf("[%s] ", account.Name), log.LstdFlags))
if err != nil {
panic(err)
}
_ui.AddTab(tab)
}
for !_ui.Exit {
if !_ui.Tick() {
time.Sleep(100 * time.Millisecond)

View File

@ -22,8 +22,7 @@ func (ctx *Context) Height() int {
return ctx.height
}
func NewContext() *Context {
width, height := termbox.Size()
func NewContext(width, height int) *Context {
return &Context{0, 0, width, height}
}

View File

@ -5,4 +5,6 @@ type Drawable interface {
Draw(ctx *Context)
// Specifies a function to call when this cell needs to be redrawn
OnInvalidate(callback func(d Drawable))
// Invalidates the drawable
Invalidate()
}

View File

@ -1,6 +1,9 @@
package ui
import "fmt"
import (
"fmt"
"math"
)
type Grid struct {
Rows []DimSpec
@ -42,6 +45,22 @@ type GridCell struct {
invalid bool
}
func NewGrid() *Grid {
return &Grid{invalid: true}
}
func (cell *GridCell) At(row, col int) *GridCell {
cell.Row = row
cell.Column = col
return cell
}
func (cell *GridCell) Span(rows, cols int) *GridCell {
cell.RowSpan = rows
cell.ColSpan = cols
return cell
}
func (grid *Grid) Draw(ctx *Context) {
invalid := grid.invalid
if invalid {
@ -51,17 +70,17 @@ func (grid *Grid) Draw(ctx *Context) {
if !cell.invalid && !invalid {
continue
}
rows := grid.rowLayout[cell.Row:cell.RowSpan]
cols := grid.columnLayout[cell.Column:cell.ColSpan]
rows := grid.rowLayout[cell.Row : cell.Row+cell.RowSpan]
cols := grid.columnLayout[cell.Column : cell.Column+cell.ColSpan]
x := cols[0].Offset
y := rows[0].Offset
width := 0
height := 0
for _, row := range rows {
width += row.Size
}
for _, col := range cols {
height += col.Size
width += col.Size
}
for _, row := range rows {
height += row.Size
}
subctx := ctx.Subcontext(x, y, width, height)
cell.Content.Draw(subctx)
@ -74,10 +93,12 @@ func (grid *Grid) reflow(ctx *Context) {
flow := func(specs *[]DimSpec, layouts *[]dimLayout, extent int) {
exact := 0
weight := 0
nweights := 0
for _, dim := range *specs {
if dim.Strategy == SIZE_EXACT {
exact += dim.Size
} else if dim.Strategy == SIZE_WEIGHT {
nweights += 1
weight += dim.Size
}
}
@ -87,30 +108,49 @@ func (grid *Grid) reflow(ctx *Context) {
if dim.Strategy == SIZE_EXACT {
layout.Size = dim.Size
} else if dim.Strategy == SIZE_WEIGHT {
size := float64(dim.Size) / float64(weight) * float64(extent)
layout.Size = int(size)
size := float64(dim.Size) / float64(weight)
size *= float64(extent - exact)
layout.Size = int(math.Floor(size))
}
offset += layout.Size
*layouts = append(*layouts, layout)
}
}
flow(&grid.Rows, &grid.rowLayout, ctx.Width())
flow(&grid.Columns, &grid.columnLayout, ctx.Height())
flow(&grid.Rows, &grid.rowLayout, ctx.Height())
flow(&grid.Columns, &grid.columnLayout, ctx.Width())
grid.invalid = false
}
func (grid *Grid) InvalidateLayout() {
func (grid *Grid) invalidateLayout() {
grid.invalid = true
if grid.onInvalidate != nil {
grid.onInvalidate(grid)
}
}
func (grid *Grid) Invalidate() {
grid.invalidateLayout()
for _, cell := range grid.Cells {
cell.Content.Invalidate()
}
}
func (grid *Grid) OnInvalidate(onInvalidate func(d Drawable)) {
grid.onInvalidate = onInvalidate
}
func (grid *Grid) AddChild(cell *GridCell) {
func (grid *Grid) AddChild(content Drawable) *GridCell {
cell := &GridCell{
RowSpan: 1,
ColSpan: 1,
Content: content,
invalid: true,
}
grid.Cells = append(grid.Cells, cell)
cell.Content.OnInvalidate(grid.cellInvalidated)
cell.invalid = true
grid.InvalidateLayout()
grid.invalidateLayout()
return cell
}
func (grid *Grid) RemoveChild(cell *GridCell) {
@ -120,7 +160,7 @@ func (grid *Grid) RemoveChild(cell *GridCell) {
break
}
}
grid.InvalidateLayout()
grid.invalidateLayout()
}
func (grid *Grid) cellInvalidated(drawable Drawable) {

View File

@ -1,41 +0,0 @@
package ui
import (
"fmt"
tb "github.com/nsf/termbox-go"
)
func TPrintf(geo *Geometry, ref tb.Cell, format string, a ...interface{}) {
str := fmt.Sprintf(format, a...)
_geo := *geo
newline := func() {
// TODO: Abort when out of room?
geo.Col = _geo.Col
geo.Row++
}
for _, ch := range str {
switch ch {
case '\n':
newline()
case '\r':
geo.Col = _geo.Col
default:
tb.SetCell(geo.Col, geo.Row, ch, ref.Fg, ref.Bg)
geo.Col++
if geo.Col == _geo.Col+geo.Width {
newline()
}
}
}
}
func TFill(geo Geometry, ref tb.Cell) {
_geo := geo
for ; geo.Row < geo.Height; geo.Row++ {
for ; geo.Col < geo.Width; geo.Col++ {
tb.SetCell(geo.Col, geo.Row, ref.Ch, ref.Fg, ref.Bg)
}
geo.Col = _geo.Col
}
}

View File

@ -1,71 +0,0 @@
package ui
import (
tb "github.com/nsf/termbox-go"
"git.sr.ht/~sircmpwn/aerc2/config"
"git.sr.ht/~sircmpwn/aerc2/worker/types"
)
const (
Valid = 0
InvalidateTabList = 1 << iota
InvalidateTabView
InvalidateStatusBar
)
const (
InvalidateAll = InvalidateTabList |
InvalidateTabView |
InvalidateStatusBar
)
type Geometry struct {
Row int
Col int
Width int
Height int
}
type AercTab interface {
Name() string
Render(at Geometry)
SetParent(parent *UIState)
}
type WorkerListener interface {
GetChannel() chan types.WorkerMessage
HandleMessage(msg types.WorkerMessage)
}
type wrappedMessage struct {
msg types.WorkerMessage
listener WorkerListener
}
type UIState struct {
Config *config.AercConfig
Exit bool
InvalidPanes uint
Panes struct {
TabList Geometry
TabView Geometry
Sidebar Geometry
StatusBar Geometry
}
Tabs []AercTab
SelectedTab int
Prompt struct {
Prompt *string
Text *string
Index int
Scroll int
}
tbEvents chan tb.Event
// Aggregate channel for all worker messages
workerEvents chan wrappedMessage
}

View File

@ -6,17 +6,27 @@ import (
"git.sr.ht/~sircmpwn/aerc2/config"
)
func Initialize(conf *config.AercConfig) (*UIState, error) {
state := UIState{
Config: conf,
InvalidPanes: InvalidateAll,
type UI struct {
Exit bool
Content Drawable
ctx *Context
tbEvents: make(chan tb.Event, 10),
workerEvents: make(chan wrappedMessage),
}
tbEvents chan tb.Event
invalidations chan interface{}
}
func Initialize(conf *config.AercConfig, content Drawable) (*UI, error) {
if err := tb.Init(); err != nil {
return nil, err
}
width, height := tb.Size()
state := UI{
Content: content,
ctx: NewContext(width, height),
tbEvents: make(chan tb.Event, 10),
invalidations: make(chan interface{}),
}
tb.SetInputMode(tb.InputEsc | tb.InputMouse)
tb.SetOutputMode(tb.Output256)
go (func() {
@ -24,50 +34,18 @@ func Initialize(conf *config.AercConfig) (*UIState, error) {
state.tbEvents <- tb.PollEvent()
}
})()
go (func() { state.invalidations <- nil })()
content.OnInvalidate(func(_ Drawable) {
go (func() { state.invalidations <- nil })()
})
return &state, nil
}
func (state *UIState) Close() {
func (state *UI) Close() {
tb.Close()
}
func (state *UIState) AddTab(tab AercTab) {
tab.SetParent(state)
state.Tabs = append(state.Tabs, tab)
if listener, ok := tab.(WorkerListener); ok {
go (func() {
for msg := range listener.GetChannel() {
state.workerEvents <- wrappedMessage{
msg: msg,
listener: listener,
}
}
})()
}
}
func (state *UIState) Invalidate(what uint) {
state.InvalidPanes |= what
}
func (state *UIState) InvalidateFrom(tab AercTab) {
if state.Tabs[state.SelectedTab] == tab {
state.Invalidate(InvalidateTabView)
}
}
func (state *UIState) calcGeometries() {
width, height := tb.Size()
// TODO: more
state.Panes.TabView = Geometry{
Row: 0,
Col: 0,
Width: width,
Height: height,
}
}
func (state *UIState) Tick() bool {
func (state *UI) Tick() bool {
select {
case event := <-state.tbEvents:
switch event.Type {
@ -76,26 +54,15 @@ func (state *UIState) Tick() bool {
state.Exit = true
}
case tb.EventResize:
state.Invalidate(InvalidateAll)
}
case msg := <-state.workerEvents:
msg.listener.HandleMessage(msg.msg)
default:
// no-op
break
}
if state.InvalidPanes != 0 {
invalid := state.InvalidPanes
state.InvalidPanes = 0
if invalid&InvalidateAll == InvalidateAll {
tb.Clear(tb.ColorDefault, tb.ColorDefault)
state.calcGeometries()
}
if invalid&InvalidateTabView != 0 {
tab := state.Tabs[state.SelectedTab]
tab.Render(state.Panes.TabView)
state.ctx = NewContext(event.Width, event.Height)
state.Content.Invalidate()
}
case <-state.invalidations:
state.Content.Draw(state.ctx)
tb.Flush()
default:
return false
}
return true
}