From 7e2f112eea11f272b49dd586d91d3f4bf99018db Mon Sep 17 00:00:00 2001 From: vicanso Date: Wed, 11 May 2022 20:29:39 +0800 Subject: [PATCH] feat: add multi line text draw --- draw.go | 29 +++++++++++ draw_test.go | 30 +++++++++++ table.go | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+) create mode 100644 table.go diff --git a/draw.go b/draw.go index f6ee73c..1708662 100644 --- a/draw.go +++ b/draw.go @@ -277,6 +277,35 @@ func (d *Draw) text(body string, x, y int) { d.Render.Text(body, x+d.Box.Left, y+d.Box.Top) } +func (d *Draw) textFit(body string, x, y, width int, style chart.Style) chart.Box { + style.TextWrap = chart.TextWrapWord + r := d.Render + lines := chart.Text.WrapFit(r, body, width, style) + style.WriteTextOptionsToRenderer(r) + var output chart.Box + + for index, line := range lines { + x0 := x + y0 := y + output.Height() + d.text(line, x0, y0) + lineBox := r.MeasureText(line) + output.Right = chart.MaxInt(lineBox.Right, output.Right) + output.Bottom += lineBox.Height() + if index < len(lines)-1 { + output.Bottom += +style.GetTextLineSpacing() + } + } + return output +} + +func (d *Draw) measureTextFit(body string, x, y, width int, style chart.Style) chart.Box { + style.TextWrap = chart.TextWrapWord + r := d.Render + lines := chart.Text.WrapFit(r, body, width, style) + style.WriteTextOptionsToRenderer(r) + return chart.Text.MeasureLines(r, lines, style) +} + func (d *Draw) lineStroke(points []Point, style LineStyle) { s := style.Style() if !s.ShouldDrawStroke() { diff --git a/draw_test.go b/draw_test.go index 694d72a..f6a3dd1 100644 --- a/draw_test.go +++ b/draw_test.go @@ -475,3 +475,33 @@ func TestDraw(t *testing.T) { assert.Equal(tt.result, string(data)) } } + +func TestDrawTextFit(t *testing.T) { + assert := assert.New(t) + d, err := NewDraw(DrawOption{ + Width: 400, + Height: 300, + }) + assert.Nil(err) + f, _ := chart.GetDefaultFont() + style := chart.Style{ + FontSize: 12, + FontColor: chart.ColorBlack, + Font: f, + } + box := d.textFit("Hello World!", 0, 20, 80, style) + assert.Equal(chart.Box{ + Right: 45, + Bottom: 35, + }, box) + + box = d.textFit("Hello World!", 0, 100, 200, style) + assert.Equal(chart.Box{ + Right: 84, + Bottom: 15, + }, box) + + buf, err := d.Bytes() + assert.Nil(err) + assert.Equal(`\nHelloWorld!Hello World!`, string(buf)) +} diff --git a/table.go b/table.go new file mode 100644 index 0000000..9cfc6b1 --- /dev/null +++ b/table.go @@ -0,0 +1,145 @@ +// MIT License + +// Copyright (c) 2022 Tree Xie + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package charts + +import ( + "errors" + + "github.com/wcharczuk/go-chart/v2" +) + +type TableOption struct { + // draw + Draw *Draw + // The width of table + Width int + // The header of table + Header []string + // The style of table + Style chart.Style + ColumnWidths []float64 + // 是否仅测量高度 + measurement bool +} + +var ErrTableColumnWidthInvalid = errors.New("table column width is invalid") + +func tableDivideWidth(width, size int, columnWidths []float64) ([]int, error) { + widths := make([]int, size) + + autoFillCount := size + restWidth := width + if len(columnWidths) != 0 { + for index, v := range columnWidths { + if v <= 0 { + continue + } + autoFillCount-- + // 小于1的表示占比 + if v < 1 { + widths[index] = int(v * float64(width)) + } else { + widths[index] = int(v) + } + restWidth -= widths[index] + } + } + if restWidth < 0 { + return nil, ErrTableColumnWidthInvalid + } + // 填充其它未指定的宽度 + if autoFillCount > 0 { + autoWidth := restWidth / autoFillCount + for index, v := range widths { + if v == 0 { + widths[index] = autoWidth + } + } + } + widthSum := 0 + for _, v := range widths { + widthSum += v + } + if widthSum > width { + return nil, ErrTableColumnWidthInvalid + } + return widths, nil +} + +func TableMeasure(opt TableOption) (chart.Box, error) { + d, err := NewDraw(DrawOption{ + Width: opt.Width, + Height: 600, + }) + if err != nil { + return chart.BoxZero, err + } + opt.Draw = d + opt.measurement = true + return tableRender(opt) +} + +func tableRender(opt TableOption) (chart.Box, error) { + if opt.Draw == nil { + return chart.BoxZero, errors.New("draw can not be nil") + } + if len(opt.Header) == 0 { + return chart.BoxZero, errors.New("header can not be nil") + } + width := opt.Width + if width == 0 { + width = opt.Draw.Box.Width() + } + + columnWidths, err := tableDivideWidth(width, len(opt.Header), opt.ColumnWidths) + if err != nil { + return chart.BoxZero, err + } + + d := opt.Draw + style := opt.Style + y := 0 + x := 0 + + headerMaxHeight := 0 + for index, text := range opt.Header { + var box chart.Box + w := columnWidths[index] + y0 := y + int(opt.Style.FontSize) + if opt.measurement { + box = d.measureTextFit(text, x, y0, w, style) + } else { + box = d.textFit(text, x, y0, w, style) + } + if box.Height() > headerMaxHeight { + headerMaxHeight = box.Height() + } + x += w + } + y += headerMaxHeight + + return chart.Box{ + Right: width, + Bottom: y, + }, nil +}