From 60b351b78c930110716b0c9db2227e13704f826d Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Sat, 17 Feb 2018 16:35:36 -0500 Subject: Polish up grid and add new rendering loop --- cmd/aerc/main.go | 50 +++++++++++++++++++++------- ui/account.go | 97 ------------------------------------------------------- ui/account.go.old | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ ui/context.go | 3 +- ui/drawable.go | 2 ++ ui/grid.go | 70 ++++++++++++++++++++++++++++++--------- ui/helpers.go | 41 ----------------------- ui/types.go | 71 ---------------------------------------- ui/ui.go | 91 +++++++++++++++++---------------------------------- 9 files changed, 223 insertions(+), 299 deletions(-) delete mode 100644 ui/account.go create mode 100644 ui/account.go.old 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 deleted file mode 100644 index 393a47a..0000000 --- a/ui/account.go +++ /dev/null @@ -1,97 +0,0 @@ -package ui - -import ( - "log" - - tb "github.com/nsf/termbox-go" - - "git.sr.ht/~sircmpwn/aerc2/config" - "git.sr.ht/~sircmpwn/aerc2/worker" - "git.sr.ht/~sircmpwn/aerc2/worker/types" -) - -type AccountTab struct { - Config *config.AccountConfig - Worker *types.Worker - Parent *UIState - logger *log.Logger - counter int -} - -func NewAccountTab(conf *config.AccountConfig, - logger *log.Logger) (*AccountTab, error) { - - work, err := worker.NewWorker(conf.Source, logger) - if err != nil { - return nil, err - } - go work.Backend.Run() - acc := &AccountTab{ - Config: conf, - Worker: work, - logger: logger, - } - acc.Worker.PostAction(&types.Configure{Config: conf}, nil) - acc.Worker.PostAction(&types.Connect{}, func(msg types.WorkerMessage) { - switch msg := msg.(type) { - case *types.Done: - acc.logger.Println("Connected.") - acc.Worker.PostAction(&types.ListDirectories{}, nil) - case *types.CertificateApprovalRequest: - // TODO: Ask the user - acc.logger.Println("Approving certificate") - acc.Worker.PostAction(&types.ApproveCertificate{ - Message: types.RespondTo(msg), - Approved: true, - }, nil) - default: - acc.logger.Println("Connection failed.") - } - }) - return acc, nil -} - -func (acc *AccountTab) Name() string { - return acc.Config.Name -} - -func (acc *AccountTab) SetParent(parent *UIState) { - acc.Parent = parent -} - -func (acc *AccountTab) Render(at Geometry) { - cell := tb.Cell{ - Ch: ' ', - Fg: tb.ColorDefault, - Bg: tb.ColorDefault, - } - TFill(at, cell) - TPrintf(&at, cell, "%s %d\n", acc.Name(), acc.counter) - acc.counter++ - if acc.counter%10000 == 0 { - acc.counter = 0 - } - acc.Parent.InvalidateFrom(acc) -} - -func (acc *AccountTab) GetChannel() chan types.WorkerMessage { - return acc.Worker.Messages -} - -func (acc *AccountTab) HandleMessage(msg types.WorkerMessage) { - msg = acc.Worker.ProcessMessage(msg) - switch msg := msg.(type) { - case *types.Done: - case *types.CertificateApprovalRequest: - case *types.Unsupported: - // no-op - case *types.Error: - acc.logger.Printf("Error: %v\n", msg.Error) - case *types.Directory: - acc.logger.Printf("Directory: %s\n", msg.Name) - default: - acc.Worker.PostAction(&types.Unsupported{ - Message: types.RespondTo(msg), - }, nil) - } -} diff --git a/ui/account.go.old b/ui/account.go.old new file mode 100644 index 0000000..393a47a --- /dev/null +++ b/ui/account.go.old @@ -0,0 +1,97 @@ +package ui + +import ( + "log" + + tb "github.com/nsf/termbox-go" + + "git.sr.ht/~sircmpwn/aerc2/config" + "git.sr.ht/~sircmpwn/aerc2/worker" + "git.sr.ht/~sircmpwn/aerc2/worker/types" +) + +type AccountTab struct { + Config *config.AccountConfig + Worker *types.Worker + Parent *UIState + logger *log.Logger + counter int +} + +func NewAccountTab(conf *config.AccountConfig, + logger *log.Logger) (*AccountTab, error) { + + work, err := worker.NewWorker(conf.Source, logger) + if err != nil { + return nil, err + } + go work.Backend.Run() + acc := &AccountTab{ + Config: conf, + Worker: work, + logger: logger, + } + acc.Worker.PostAction(&types.Configure{Config: conf}, nil) + acc.Worker.PostAction(&types.Connect{}, func(msg types.WorkerMessage) { + switch msg := msg.(type) { + case *types.Done: + acc.logger.Println("Connected.") + acc.Worker.PostAction(&types.ListDirectories{}, nil) + case *types.CertificateApprovalRequest: + // TODO: Ask the user + acc.logger.Println("Approving certificate") + acc.Worker.PostAction(&types.ApproveCertificate{ + Message: types.RespondTo(msg), + Approved: true, + }, nil) + default: + acc.logger.Println("Connection failed.") + } + }) + return acc, nil +} + +func (acc *AccountTab) Name() string { + return acc.Config.Name +} + +func (acc *AccountTab) SetParent(parent *UIState) { + acc.Parent = parent +} + +func (acc *AccountTab) Render(at Geometry) { + cell := tb.Cell{ + Ch: ' ', + Fg: tb.ColorDefault, + Bg: tb.ColorDefault, + } + TFill(at, cell) + TPrintf(&at, cell, "%s %d\n", acc.Name(), acc.counter) + acc.counter++ + if acc.counter%10000 == 0 { + acc.counter = 0 + } + acc.Parent.InvalidateFrom(acc) +} + +func (acc *AccountTab) GetChannel() chan types.WorkerMessage { + return acc.Worker.Messages +} + +func (acc *AccountTab) HandleMessage(msg types.WorkerMessage) { + msg = acc.Worker.ProcessMessage(msg) + switch msg := msg.(type) { + case *types.Done: + case *types.CertificateApprovalRequest: + case *types.Unsupported: + // no-op + case *types.Error: + acc.logger.Printf("Error: %v\n", msg.Error) + case *types.Directory: + acc.logger.Printf("Directory: %s\n", msg.Name) + default: + acc.Worker.PostAction(&types.Unsupported{ + Message: types.RespondTo(msg), + }, nil) + } +} 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 } -- cgit v1.2.3