From 60b351b78c930110716b0c9db2227e13704f826d Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Sat, 17 Feb 2018 16:35:36 -0500 Subject: [PATCH] Polish up grid and add new rendering loop --- cmd/aerc/main.go | 50 +++++++++++++---- ui/{account.go => account.go.old} | 0 ui/context.go | 3 +- ui/drawable.go | 2 + ui/grid.go | 70 +++++++++++++++++++----- ui/helpers.go | 41 -------------- ui/types.go | 71 ------------------------ ui/ui.go | 91 ++++++++++--------------------- 8 files changed, 126 insertions(+), 202 deletions(-) rename ui/{account.go => account.go.old} (100%) delete mode 100644 ui/helpers.go delete mode 100644 ui/types.go diff --git a/cmd/aerc/main.go b/cmd/aerc/main.go index 4219978..1d11c5d 100644 --- a/cmd/aerc/main.go +++ b/cmd/aerc/main.go @@ -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) diff --git a/ui/account.go b/ui/account.go.old similarity index 100% rename from ui/account.go rename to ui/account.go.old diff --git a/ui/context.go b/ui/context.go index 9f2e2fe..e7d9ebe 100644 --- a/ui/context.go +++ b/ui/context.go @@ -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} } diff --git a/ui/drawable.go b/ui/drawable.go index a61c020..ef09451 100644 --- a/ui/drawable.go +++ b/ui/drawable.go @@ -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() } diff --git a/ui/grid.go b/ui/grid.go index 2183a55..2091fc5 100644 --- a/ui/grid.go +++ b/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) { diff --git a/ui/helpers.go b/ui/helpers.go deleted file mode 100644 index f2b2adf..0000000 --- a/ui/helpers.go +++ /dev/null @@ -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 - } -} diff --git a/ui/types.go b/ui/types.go deleted file mode 100644 index 5437642..0000000 --- a/ui/types.go +++ /dev/null @@ -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 -} diff --git a/ui/ui.go b/ui/ui.go index db31696..d1d2ca3 100644 --- a/ui/ui.go +++ b/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 }