Polish up grid and add new rendering loop
This commit is contained in:
parent
1892d73161
commit
60b351b78c
8 changed files with 126 additions and 202 deletions
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
70
ui/grid.go
70
ui/grid.go
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
71
ui/types.go
71
ui/types.go
|
@ -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
|
||||
}
|
89
ui/ui.go
89
ui/ui.go
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue