aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobin Jarry <robin@jarry.cc>2023-02-03 13:14:35 +0100
committerRobin Jarry <robin@jarry.cc>2023-02-20 14:48:42 +0100
commit6cfbc87d8ab0be8d264d81b0b1f26e7b96719dfc (patch)
treec09b6b671bd90504ce0e90512b39b31f650f37fb
parentd74400ac07a9f149e89fdf2b7232ffc6871f8553 (diff)
downloadaerc-6cfbc87d8ab0be8d264d81b0b1f26e7b96719dfc.tar.gz
aerc-6cfbc87d8ab0be8d264d81b0b1f26e7b96719dfc.zip
dirlist: use templates instead of % mini language
Replace dirlist-format with two settings: dirlist-left & dirlist-right. These two settings take aerc-templates(7) and may be left empty. Add automatic translation of dirlist-format to these new settings. Display a warning on startup if dirlist-format has been converted. Signed-off-by: Robin Jarry <robin@jarry.cc> Acked-by: Tim Culverhouse <tim@timculverhouse.com>
-rw-r--r--config/aerc.conf13
-rw-r--r--config/templates.go1
-rw-r--r--config/ui.go101
-rw-r--r--doc/aerc-config.5.scd28
-rw-r--r--doc/aerc-templates.7.scd2
-rw-r--r--lib/state/templates.go14
-rw-r--r--models/templates.go1
-rw-r--r--widgets/dirlist.go175
-rw-r--r--widgets/dirtree.go55
9 files changed, 243 insertions, 147 deletions
diff --git a/config/aerc.conf b/config/aerc.conf
index 38049b77..d4e964a9 100644
--- a/config/aerc.conf
+++ b/config/aerc.conf
@@ -136,10 +136,17 @@
# Default: `
#pinned-tab-marker='`'
-# Describes the format string to use for the directory list
+# Template for the left side of the directory list.
+# See aerc-templates(7) for all available fields and functions.
#
-# Default: %n %>r
-#dirlist-format=%n %>r
+# Default: {{.Folder}}
+#dirlist-left={{.Folder}}
+
+# Template for the right side of the directory list.
+# See aerc-templates(7) for all available fields and functions.
+#
+# Default: {{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}}
+#dirlist-right={{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}}
# Delay after which the messages are actually listed when entering a directory.
# This avoids loading messages when skipping over folders and makes the UI more
diff --git a/config/templates.go b/config/templates.go
index 0f3870c5..bc05be66 100644
--- a/config/templates.go
+++ b/config/templates.go
@@ -104,6 +104,7 @@ func (d *dummyData) OriginalHeader(string) string { return "" }
func (d *dummyData) Recent(...string) int { return 1 }
func (d *dummyData) Unread(...string) int { return 3 }
func (d *dummyData) Exists(...string) int { return 14 }
+func (d *dummyData) RUE(...string) string { return "1/3/14" }
func (d *dummyData) Connected() bool { return false }
func (d *dummyData) ConnectionInfo() string { return "" }
func (d *dummyData) ContentInfo() string { return "" }
diff --git a/config/ui.go b/config/ui.go
index d4b36f34..599651d1 100644
--- a/config/ui.go
+++ b/config/ui.go
@@ -22,6 +22,10 @@ type UIConfig struct {
// deprecated
IndexFormat string `ini:"index-format"`
+ DirListFormat string `ini:"dirlist-format"` // deprecated
+ DirListLeft *template.Template `ini:"-"`
+ DirListRight *template.Template `ini:"-"`
+
AutoMarkRead bool `ini:"auto-mark-read"`
TimestampFormat string `ini:"timestamp-format"`
ThisDayTimeFormat string `ini:"this-day-time-format"`
@@ -52,7 +56,6 @@ type UIConfig struct {
IconUnknown string `ini:"icon-unknown"`
IconInvalid string `ini:"icon-invalid"`
IconAttachment string `ini:"icon-attachment"`
- DirListFormat string `ini:"dirlist-format"`
DirListDelay time.Duration `ini:"dirlist-delay"`
DirListTree bool `ini:"dirlist-tree"`
DirListCollapse int `ini:"dirlist-collapse"`
@@ -101,15 +104,20 @@ type uiContextKey struct {
value string
}
+const unreadExists string = `{{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}}`
+
func defaultUiConfig() *UIConfig {
date, _ := templates.ParseTemplate("column-date", "{{.DateAutoFormat .Date.Local}}")
name, _ := templates.ParseTemplate("column-name", "{{index (.From | names) 0}}")
flags, _ := templates.ParseTemplate("column-flags", `{{.Flags | join ""}}`)
subject, _ := templates.ParseTemplate("column-subject", "{{.Subject}}")
+ left, _ := templates.ParseTemplate("folder", "{{.Folder}}")
+ right, _ := templates.ParseTemplate("ue", unreadExists)
tabTitleAccount, _ := templates.ParseTemplate("tab-title-account", "{{.Account}}")
tabTitleComposer, _ := templates.ParseTemplate("tab-title-composer", "{{.Subject}}")
return &UIConfig{
- IndexFormat: "", // deprecated
+ IndexFormat: "", // deprecated
+ DirListFormat: "", // deprecated
IndexColumns: []*ColumnDef{
{
Name: "date",
@@ -135,6 +143,8 @@ func defaultUiConfig() *UIConfig {
Template: subject,
},
},
+ DirListLeft: left,
+ DirListRight: right,
ColumnSeparator: " ",
AutoMarkRead: true,
TimestampFormat: "2006-01-02 03:04 PM",
@@ -162,7 +172,6 @@ func defaultUiConfig() *UIConfig {
IconUnknown: "[s?]",
IconInvalid: "[s!]",
IconAttachment: "a",
- DirListFormat: "%n %>r",
DirListDelay: 200 * time.Millisecond,
NextMessageOnDelete: true,
CompletionDelay: 250 * time.Millisecond,
@@ -357,6 +366,58 @@ index-format will be removed in aerc 0.17.
}
Warnings = append(Warnings, w)
}
+ left, _ := section.GetKey("dirlist-left")
+ if left != nil {
+ t, err := templates.ParseTemplate(left.String(), left.String())
+ if err != nil {
+ return err
+ }
+ config.DirListLeft = t
+ }
+ right, _ := section.GetKey("dirlist-right")
+ if right != nil {
+ t, err := templates.ParseTemplate(right.String(), right.String())
+ if err != nil {
+ return err
+ }
+ config.DirListRight = t
+ }
+ if left == nil && right == nil && config.DirListFormat != "" {
+ left, right := convertDirlistFormat(config.DirListFormat)
+ l, err := templates.ParseTemplate(left, left)
+ if err != nil {
+ return err
+ }
+ r, err := templates.ParseTemplate(right, right)
+ if err != nil {
+ return err
+ }
+ config.DirListLeft = l
+ config.DirListRight = r
+ log.Warnf("%s %s",
+ "The dirlist-format setting has been replaced by dirlist-left and dirlist-right.",
+ "dirlist-format will be removed in aerc 0.17.")
+ w := Warning{
+ Title: "DEPRECATION WARNING: [" + section.Name() + "].dirlist-format",
+ Body: fmt.Sprintf(`
+The dirlist-format setting is deprecated. It has been replaced by dirlist-left
+and dirlist-right.
+
+Your configuration in this instance was automatically converted to:
+
+[%s]
+dirlist-left = %s
+dirlist-right = %s
+
+Your configuration file was not changed. To make this change permanent and to
+dismiss this deprecation warning on launch, copy the above lines into aerc.conf
+and remove dirlist-format from it. See aerc-config(5) for more details.
+
+dirlist-format will be removed in aerc 0.17.
+`, section.Name(), left, right),
+ }
+ Warnings = append(Warnings, w)
+ }
if key, err := section.GetKey("tab-title-account"); err == nil {
val := key.Value()
tmpl, err := templates.ParseTemplate("tab-title-account", val)
@@ -377,7 +438,7 @@ index-format will be removed in aerc 0.17.
return nil
}
-var indexFmtRegexp = regexp.MustCompile(`%(-?\d+)?(\.\d+)?([A-Za-z%])`)
+var indexFmtRegexp = regexp.MustCompile(`%(-?\d+)?(\.\d+)?([ACDFRTZadfgilnrstuv])`)
func convertIndexFormat(indexFormat string) ([]*ColumnDef, error) {
matches := indexFmtRegexp.FindAllStringSubmatch(indexFormat, -1)
@@ -499,6 +560,38 @@ func indexVerbToTemplate(verb rune) (f, name string) {
return
}
+func convertDirlistFormat(format string) (string, string) {
+ tmpl := regexp.MustCompile(`%>?[Nnr]`).ReplaceAllStringFunc(
+ format,
+ func(s string) string {
+ runes := []rune(s)
+ switch runes[len(runes)-1] {
+ case 'N':
+ s = `{{.Folder | compactDir}}`
+ case 'n':
+ s = `{{.Folder}}`
+ case 'r':
+ s = unreadExists
+ default:
+ return s
+ }
+ if strings.HasPrefix(string(runes), "%>") {
+ s = "%>" + s
+ }
+ return s
+ },
+ )
+ tokens := strings.SplitN(tmpl, "%>", 1)
+ switch len(tokens) {
+ case 2:
+ return tokens[0], tokens[1]
+ case 1:
+ return tokens[0], ""
+ default:
+ return "", ""
+ }
+}
+
func (ui *UIConfig) loadStyleSet(styleSetDirs []string) error {
ui.style = NewStyleSet()
err := ui.style.LoadStyleSet(ui.StyleSetName, styleSetDirs)
diff --git a/doc/aerc-config.5.scd b/doc/aerc-config.5.scd
index 4d995d6b..a04f3be9 100644
--- a/doc/aerc-config.5.scd
+++ b/doc/aerc-config.5.scd
@@ -211,23 +211,17 @@ These options are configured in the *[ui]* section of _aerc.conf_.
Example:
*sort* = _from -r date_
-*dirlist-format* = _<format>_
- Describes the format string to use for the directory list.
-
- Default: _%n %>r_
-
-[- *Format specifier*
-:[ *Description*
-| _%%_
-: literal %
-| _%n_
-: directory name
-| _%N_
-: compacted directory name
-| _%r_
-: recent/unseen/total message count
-| _%>X_
-: make format specifier 'X' be right justified
+*dirlist-left* = _<go template>_
+ Template for the left side of the directory list.
+ See *aerc-templates*(7) for all available fields and functions.
+
+ Default: _{{.Folder}}_
+
+*dirlist-right* = _<go template>_
+ Template for the right side of the directory list.
+ See *aerc-templates*(7) for all available fields and functions.
+
+ Default: _{{if .Unread}}{{humanReadable .Unread}}/{{end}}{{if .Exists}}{{humanReadable .Exists}}{{end}}_
*dirlist-delay* = _<duration>_
Delay after which the messages are actually listed when entering
diff --git a/doc/aerc-templates.7.scd b/doc/aerc-templates.7.scd
index 91745e8c..83b19ac9 100644
--- a/doc/aerc-templates.7.scd
+++ b/doc/aerc-templates.7.scd
@@ -152,6 +152,7 @@ available always.
```
{{.Recent}} {{.Unread}} {{.Exists}}
+ {{.RUE}}
```
Current message counts for specific folders:
@@ -160,6 +161,7 @@ available always.
{{.Recent "inbox"}}
{{.Unread "inbox" "aerc/pending"}}
{{.Exists "archive" "spam" "foo/baz" "foo/bar"}}
+ {{.RUE "inbox"}}
```
*Status line*
diff --git a/lib/state/templates.go b/lib/state/templates.go
index f37c4865..2d5e39f5 100644
--- a/lib/state/templates.go
+++ b/lib/state/templates.go
@@ -1,6 +1,7 @@
package state
import (
+ "fmt"
"strings"
"time"
@@ -369,6 +370,19 @@ func (d *TemplateData) Exists(folders ...string) int {
return e
}
+func (d *TemplateData) RUE(folders ...string) string {
+ r, u, e := d.rue(folders...)
+ switch {
+ case r > 0:
+ return fmt.Sprintf("%d/%d/%d", r, u, e)
+ case u > 0:
+ return fmt.Sprintf("%d/%d", u, e)
+ case e > 0:
+ return fmt.Sprintf("%d", e)
+ }
+ return ""
+}
+
func (d *TemplateData) Connected() bool {
if d.state != nil {
return d.state.Connected
diff --git a/models/templates.go b/models/templates.go
index c07f3dbd..4886b83c 100644
--- a/models/templates.go
+++ b/models/templates.go
@@ -33,6 +33,7 @@ type TemplateData interface {
Recent(folders ...string) int
Unread(folders ...string) int
Exists(folders ...string) int
+ RUE(folders ...string) string
Connected() bool
ConnectionInfo() string
ContentInfo() string
diff --git a/widgets/dirlist.go b/widgets/dirlist.go
index 93db0763..1e1a8a61 100644
--- a/widgets/dirlist.go
+++ b/widgets/dirlist.go
@@ -1,13 +1,12 @@
package widgets
import (
+ "bytes"
"context"
- "fmt"
"math"
"os"
"regexp"
"sort"
- "strings"
"time"
"github.com/gdamore/tcell/v2"
@@ -16,6 +15,8 @@ import (
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
"git.sr.ht/~rjarry/aerc/lib/format"
+ "git.sr.ht/~rjarry/aerc/lib/state"
+ "git.sr.ht/~rjarry/aerc/lib/templates"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/log"
"git.sr.ht/~rjarry/aerc/models"
@@ -184,70 +185,6 @@ func (dirlist *DirectoryList) Invalidate() {
ui.Invalidate()
}
-func (dirlist *DirectoryList) getDirString(name string, width int, recentUnseen func() string) string {
- percent := false
- rightJustify := false
- formatted := ""
- doRightJustify := func(s string) {
- formatted = runewidth.FillRight(formatted, width-len(s))
- formatted = runewidth.Truncate(formatted, width-len(s), "…")
- }
- for _, char := range dirlist.UiConfig(name).DirListFormat {
- switch char {
- case '%':
- if percent {
- formatted += string(char)
- percent = false
- } else {
- percent = true
- }
- case '>':
- if percent {
- rightJustify = true
- }
- case 'N':
- name = format.CompactPath(name, os.PathSeparator)
- fallthrough
- case 'n':
- if percent {
- if rightJustify {
- doRightJustify(name)
- rightJustify = false
- }
- formatted += name
- percent = false
- }
- case 'r':
- if percent {
- rString := recentUnseen()
- if rightJustify {
- doRightJustify(rString)
- rightJustify = false
- }
- formatted += rString
- percent = false
- }
- default:
- formatted += string(char)
- }
- }
- return formatted
-}
-
-func (dirlist *DirectoryList) getRUEString(name string) string {
- r, u, e := dirlist.GetRUECount(name)
- rueString := ""
- switch {
- case r > 0:
- rueString = fmt.Sprintf("%d/%d/%d", r, u, e)
- case u > 0:
- rueString = fmt.Sprintf("%d/%d", u, e)
- case e > 0:
- rueString = fmt.Sprintf("%d", e)
- }
- return rueString
-}
-
// Returns the Recent, Unread, and Exist counts for the named directory
func (dirlist *DirectoryList) GetRUECount(name string) (int, int, int) {
msgStore, ok := dirlist.MsgStore(name)
@@ -262,8 +199,9 @@ func (dirlist *DirectoryList) GetRUECount(name string) (int, int, int) {
}
func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
+ uiConfig := dirlist.UiConfig("")
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
- dirlist.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT))
+ uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT))
if dirlist.spinner.IsRunning() {
dirlist.spinner.Draw(ctx)
@@ -271,8 +209,8 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
}
if len(dirlist.dirs) == 0 {
- style := dirlist.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT)
- ctx.Printf(0, 0, style, dirlist.UiConfig("").EmptyDirlist)
+ style := uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT)
+ ctx.Printf(0, 0, style, uiConfig.EmptyDirlist)
return
}
@@ -284,9 +222,14 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
textWidth -= 1
}
if textWidth < 0 {
- textWidth = 0
+ return
}
+ listCtx := ctx.Subcontext(0, 0, textWidth, ctx.Height())
+ var data state.TemplateData
+
+ data.SetAccount(dirlist.acctConf)
+
for i, name := range dirlist.dirs {
if i < dirlist.Scroll() {
continue
@@ -296,27 +239,13 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
break
}
- dirStyle := []config.StyleObject{}
- s := dirlist.getRUEString(name)
- switch strings.Count(s, "/") {
- case 1:
- dirStyle = append(dirStyle, config.STYLE_DIRLIST_UNREAD)
- case 2:
- dirStyle = append(dirStyle, config.STYLE_DIRLIST_RECENT)
- }
- style := dirlist.UiConfig(name).GetComposedStyle(
- config.STYLE_DIRLIST_DEFAULT, dirStyle)
- if name == dirlist.selecting {
- style = dirlist.UiConfig(name).GetComposedStyleSelected(
- config.STYLE_DIRLIST_DEFAULT, dirStyle)
- }
- ctx.Fill(0, row, textWidth, 1, ' ', style)
-
- dirString := dirlist.getDirString(name, textWidth, func() string {
- return s
- })
-
- ctx.Printf(0, row, style, dirString)
+ data.SetFolder(name)
+ data.SetRUE([]string{name}, dirlist.GetRUECount)
+ left, right, style := dirlist.renderDir(
+ name, uiConfig, &data,
+ name == dirlist.selecting, listCtx.Width(),
+ )
+ listCtx.Printf(0, row, style, "%s %s", left, right)
}
if dirlist.NeedScrollbar() {
@@ -325,6 +254,70 @@ func (dirlist *DirectoryList) Draw(ctx *ui.Context) {
}
}
+func (dirlist *DirectoryList) renderDir(
+ path string, conf *config.UIConfig, data *state.TemplateData,
+ selected bool, width int,
+) (string, string, tcell.Style) {
+ var left, right string
+ var buf bytes.Buffer
+
+ var styles []config.StyleObject
+ var style tcell.Style
+
+ r, u, _ := dirlist.GetRUECount(path)
+ switch {
+ case r > 0:
+ styles = append(styles, config.STYLE_DIRLIST_RECENT)
+ case u > 0:
+ styles = append(styles, config.STYLE_DIRLIST_UNREAD)
+ }
+ conf = conf.ForFolder(path)
+ if selected {
+ style = conf.GetComposedStyleSelected(
+ config.STYLE_DIRLIST_DEFAULT, styles)
+ } else {
+ style = conf.GetComposedStyle(
+ config.STYLE_DIRLIST_DEFAULT, styles)
+ }
+
+ err := templates.Render(conf.DirListLeft, &buf, data)
+ if err != nil {
+ log.Errorf("dirlist-left: %s", err)
+ left = err.Error()
+ style = conf.GetStyle(config.STYLE_ERROR)
+ } else {
+ left = buf.String()
+ }
+ buf.Reset()
+ err = templates.Render(conf.DirListRight, &buf, data)
+ if err != nil {
+ log.Errorf("dirlist-right: %s", err)
+ right = err.Error()
+ style = conf.GetStyle(config.STYLE_ERROR)
+ } else {
+ right = buf.String()
+ }
+ buf.Reset()
+
+ lwidth := runewidth.StringWidth(left)
+ rwidth := runewidth.StringWidth(right)
+
+ if lwidth+rwidth+1 > width {
+ if rwidth > 3*width/4 {
+ rwidth = 3 * width / 4
+ }
+ lwidth = width - rwidth - 1
+ right = runewidth.FillLeft(right, rwidth)
+ right = format.TruncateHead(right, rwidth, "…")
+ left = runewidth.FillRight(left, lwidth)
+ left = runewidth.Truncate(left, lwidth, "…")
+ } else {
+ left = runewidth.FillRight(left, width-rwidth-1)
+ }
+
+ return left, right, style
+}
+
func (dirlist *DirectoryList) drawScrollbar(ctx *ui.Context) {
gutterStyle := tcell.StyleDefault
pillStyle := tcell.StyleDefault.Reverse(true)
diff --git a/widgets/dirtree.go b/widgets/dirtree.go
index e9fbf061..0c7f090a 100644
--- a/widgets/dirtree.go
+++ b/widgets/dirtree.go
@@ -8,6 +8,7 @@ import (
"git.sr.ht/~rjarry/aerc/config"
"git.sr.ht/~rjarry/aerc/lib"
+ "git.sr.ht/~rjarry/aerc/lib/state"
"git.sr.ht/~rjarry/aerc/lib/ui"
"git.sr.ht/~rjarry/aerc/log"
"git.sr.ht/~rjarry/aerc/worker/types"
@@ -51,8 +52,9 @@ func (dt *DirectoryTree) UpdateList(done func([]string)) {
}
func (dt *DirectoryTree) Draw(ctx *ui.Context) {
+ uiConfig := dt.UiConfig("")
ctx.Fill(0, 0, ctx.Width(), ctx.Height(), ' ',
- dt.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT))
+ uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT))
if dt.DirectoryList.spinner.IsRunning() {
dt.DirectoryList.spinner.Draw(ctx)
@@ -61,8 +63,8 @@ func (dt *DirectoryTree) Draw(ctx *ui.Context) {
n := dt.countVisible(dt.list)
if n == 0 || dt.listIdx < 0 {
- style := dt.UiConfig("").GetStyle(config.STYLE_DIRLIST_DEFAULT)
- ctx.Printf(0, 0, style, dt.UiConfig("").EmptyDirlist)
+ style := uiConfig.GetStyle(config.STYLE_DIRLIST_DEFAULT)
+ ctx.Printf(0, 0, style, uiConfig.EmptyDirlist)
return
}
@@ -80,46 +82,35 @@ func (dt *DirectoryTree) Draw(ctx *ui.Context) {
textWidth -= 1
}
if textWidth < 0 {
- textWidth = 0
+ return
}
+ treeCtx := ctx.Subcontext(0, 0, textWidth, ctx.Height())
+ var data state.TemplateData
+
+ data.SetAccount(dt.acctConf)
+
+ n = 0
for i, node := range dt.list {
+ if n > treeCtx.Height() {
+ break
+ }
rowNr := dt.countVisible(dt.list[:i])
if rowNr < dt.Scroll() || !isVisible(node) {
continue
}
- row := rowNr - dt.Scroll()
- if row >= ctx.Height() {
- break
- }
- name := dt.displayText(node)
-
- dirStyle := []config.StyleObject{}
path := dt.getDirectory(node)
- s := dt.getRUEString(path)
- switch strings.Count(s, "/") {
- case 1:
- dirStyle = append(dirStyle, config.STYLE_DIRLIST_UNREAD)
- case 2:
- dirStyle = append(dirStyle, config.STYLE_DIRLIST_RECENT)
- }
- style := dt.UiConfig(path).GetComposedStyle(
- config.STYLE_DIRLIST_DEFAULT, dirStyle)
- if i == dt.listIdx {
- style = dt.UiConfig(path).GetComposedStyleSelected(
- config.STYLE_DIRLIST_DEFAULT, dirStyle)
- }
- ctx.Fill(0, row, textWidth, 1, ' ', style)
+ data.SetFolder(dt.displayText(node))
+ data.SetRUE([]string{path}, dt.GetRUECount)
- dirString := dt.getDirString(name, textWidth, func() string {
- if path != "" {
- return s
- }
- return ""
- })
+ left, right, style := dt.renderDir(
+ path, uiConfig, &data,
+ i == dt.listIdx, treeCtx.Width(),
+ )
- ctx.Printf(0, row, style, dirString)
+ treeCtx.Printf(0, n, style, "%s %s", left, right)
+ n++
}
if dt.NeedScrollbar() {