From 712ff0cab551b525b7c8564d276261b712411040 Mon Sep 17 00:00:00 2001 From: Mats Linander Date: Tue, 4 Jun 2024 14:04:00 -0400 Subject: [PATCH] render: make widget trees JSON serializable --- render/animation.go | 27 ++++- render/animation/positioned.go | 3 +- render/animation/transformation.go | 2 +- render/box.go | 59 +++++++++- render/circle.go | 48 +++++++- render/colors.go | 14 +-- render/colors_test.go | 6 +- render/column.go | 26 ++++- render/image.go | 39 ++++++- render/marquee.go | 23 +++- render/padding.go | 6 +- render/pie_chart.go | 53 ++++++++- render/plot.go | 108 ++++++++++++++++-- render/plot_test.go | 8 +- render/root.go | 21 ++++ render/row.go | 26 ++++- render/sequence.go | 2 +- render/sequence_test.go | 2 +- render/stack.go | 3 +- render/starfield.go | 2 +- render/text.go | 51 ++++++++- render/tracer.go | 3 +- render/vector.go | 26 ++++- render/widget.go | 44 +++++++ render/wrappedtext.go | 6 +- runtime/gen/main.go | 12 +- runtime/gen/type.tmpl | 1 + .../modules/animation_runtime/generated.go | 2 + runtime/modules/render_runtime/data.go | 4 +- runtime/modules/render_runtime/generated.go | 14 +++ 30 files changed, 575 insertions(+), 66 deletions(-) diff --git a/render/animation.go b/render/animation.go index bf1344ded3..dcdbdf8987 100644 --- a/render/animation.go +++ b/render/animation.go @@ -1,6 +1,7 @@ package render import ( + "encoding/json" "image" "github.com/tidbyt/gg" @@ -26,7 +27,8 @@ import ( // ) // EXAMPLE END type Animation struct { - Widget + Type string `starlark:"-"` + Children []Widget } @@ -63,3 +65,26 @@ func (a Animation) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { a.Children[ModInt(frameIdx, len(a.Children))].Paint(dc, bounds, frameIdx) } + +func (a *Animation) UnmarshalJSON(data []byte) error { + type Alias Animation + aux := &struct { + Children []json.RawMessage + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + children := []Widget{} + for _, childData := range aux.Children { + child, err := UnmarshalWidgetJSON(childData) + if err != nil { + return err + } + children = append(children, child) + } + a.Children = children + return nil +} diff --git a/render/animation/positioned.go b/render/animation/positioned.go index bfe710ab3d..f8203e6ea3 100644 --- a/render/animation/positioned.go +++ b/render/animation/positioned.go @@ -24,7 +24,8 @@ import ( // DOC(Hold): Delay after animation in frames // type AnimatedPositioned struct { - render.Widget + Type string `starlark:"-"` + Child render.Widget `starlark:"child,required"` XStart int `starlark:"x_start"` XEnd int `starlark:"x_end"` diff --git a/render/animation/transformation.go b/render/animation/transformation.go index 46d03e7f1f..8ec40a01ff 100644 --- a/render/animation/transformation.go +++ b/render/animation/transformation.go @@ -162,7 +162,7 @@ func findKeyframes(arr []Keyframe, p float64) (Keyframe, Keyframe, error) { // ), // EXAMPLE END type Transformation struct { - render.Widget + Type string `starlark:"-"` Child render.Widget `starlark:"child,required"` Keyframes []Keyframe `starlark:"keyframes,required"` diff --git a/render/box.go b/render/box.go index a23e8bbf39..3b3d2570c0 100644 --- a/render/box.go +++ b/render/box.go @@ -1,6 +1,8 @@ package render import ( + "encoding/json" + "fmt" "image" "image/color" @@ -31,11 +33,12 @@ import ( // ) // EXAMPLE END type Box struct { - Widget + Type string `starlark:"-"` + Child Widget Width, Height int Padding int - Color color.Color + Color color.RGBA } func (b Box) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle { @@ -58,7 +61,7 @@ func (b Box) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { h = bounds.Dy() } - if b.Color != nil { + if b.Color != (color.RGBA{}) { dc.SetColor(b.Color) dc.DrawRectangle(0, 0, float64(w), float64(h)) dc.Fill() @@ -103,3 +106,53 @@ func (b Box) FrameCount() int { } return 1 } + +func (b *Box) UnmarshalJSON(data []byte) error { + type Alias Box + aux := &struct { + Child json.RawMessage + Color string + *Alias + }{ + Alias: (*Alias)(b), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if aux.Child != nil { + child, err := UnmarshalWidgetJSON(aux.Child) + if err != nil { + return err + } + b.Child = child + } + + if aux.Color != "" { + col, err := ParseColor(aux.Color) + if err != nil { + return err + } + b.Color = col + } + + return nil +} + +func (b *Box) MarshalJSON() ([]byte, error) { + col := "" + if b.Color != (color.RGBA{}) { + r, g, b, a := b.Color.RGBA() + col = fmt.Sprintf("#%02x%02x%02x%02x", r>>8, g>>8, b>>8, a>>8) + } + type Alias Box + aux := &struct { + *Alias + Color string + }{ + Alias: (*Alias)(b), + Color: col, + } + + return json.Marshal(aux) +} diff --git a/render/circle.go b/render/circle.go index c12727216c..8480db2b31 100644 --- a/render/circle.go +++ b/render/circle.go @@ -1,6 +1,8 @@ package render import ( + "encoding/json" + "fmt" "image" "image/color" "math" @@ -24,11 +26,11 @@ import ( // ) // EXAMPLE END type Circle struct { - Widget + Type string `starlark:"-"` Child Widget - Color color.Color `starlark:"color, required"` - Diameter int `starlark:"diameter,required"` + Color color.RGBA `starlark:"color, required"` + Diameter int `starlark:"diameter,required"` } func (c Circle) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle { @@ -69,3 +71,43 @@ func (c Circle) FrameCount() int { } return 1 } + +func (c *Circle) UnmarshalJSON(data []byte) error { + type Alias Circle + aux := &struct { + Child json.RawMessage + *Alias + }{ + Alias: (*Alias)(c), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if aux.Child != nil { + child, err := UnmarshalWidgetJSON(aux.Child) + if err != nil { + return err + } + c.Child = child + } + return nil +} + +func (c *Circle) MarshalJSON() ([]byte, error) { + col := "" + if c.Color != (color.RGBA{}) { + r, g, b, a := c.Color.RGBA() + col = fmt.Sprintf("#%02x%02x%02x%02x", r>>8, g>>8, b>>8, a>>8) + } + type Alias Circle + aux := &struct { + *Alias + Color string + }{ + Alias: (*Alias)(c), + Color: col, + } + + return json.Marshal(aux) +} diff --git a/render/colors.go b/render/colors.go index cf14239e7f..b32ca59908 100644 --- a/render/colors.go +++ b/render/colors.go @@ -6,7 +6,7 @@ import ( "strings" ) -func ParseColor(scol string) (color.Color, error) { +func ParseColor(scol string) (color.RGBA, error) { var format string var fourBits bool var hasAlpha bool @@ -31,7 +31,7 @@ func ParseColor(scol string) (color.Color, error) { fourBits = false hasAlpha = true default: - return color.Gray{0}, fmt.Errorf("color: %v is not a hex-color", scol) + return color.RGBA{}, fmt.Errorf("color: %v is not a hex-color", scol) } var r, g, b, a uint8 @@ -39,18 +39,18 @@ func ParseColor(scol string) (color.Color, error) { if hasAlpha { n, err := fmt.Sscanf(scol, format, &r, &g, &b, &a) if err != nil { - return color.Gray{0}, err + return color.RGBA{}, err } if n != 4 { - return color.Gray{0}, fmt.Errorf("color: %v is not a hex-color", scol) + return color.RGBA{}, fmt.Errorf("color: %v is not a hex-color", scol) } } else { n, err := fmt.Sscanf(scol, format, &r, &g, &b) if err != nil { - return color.Gray{0}, err + return color.RGBA{}, err } if n != 3 { - return color.Gray{0}, fmt.Errorf("color: %v is not a hex-color", scol) + return color.RGBA{}, fmt.Errorf("color: %v is not a hex-color", scol) } if fourBits { a = 15 @@ -66,5 +66,5 @@ func ParseColor(scol string) (color.Color, error) { a |= a << 4 } - return color.NRGBA{r, g, b, a}, nil + return color.RGBA{r, g, b, a}, nil } diff --git a/render/colors_test.go b/render/colors_test.go index 8110195f84..2def7ef98d 100644 --- a/render/colors_test.go +++ b/render/colors_test.go @@ -1,7 +1,6 @@ package render import ( - "image/color" "testing" "github.com/stretchr/testify/assert" @@ -15,12 +14,9 @@ func ParseAndAssertColor( expectedB uint8, expectedA uint8, ) { - col, err := ParseColor(scol) + c, err := ParseColor(scol) assert.Nil(t, err) - c, ok := col.(color.NRGBA) - assert.True(t, ok) - assert.Equal(t, expectedR, c.R) assert.Equal(t, expectedG, c.G) assert.Equal(t, expectedB, c.B) diff --git a/render/column.go b/render/column.go index a2135b655c..81d9a1d48b 100644 --- a/render/column.go +++ b/render/column.go @@ -1,6 +1,7 @@ package render import ( + "encoding/json" "image" "github.com/tidbyt/gg" @@ -56,7 +57,7 @@ import ( // ) // EXAMPLE END type Column struct { - Widget + Type string `starlark:"-"` Children []Widget `starlark:"children,required"` MainAlign string `starlark:"main_align"` @@ -89,3 +90,26 @@ func (c Column) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { func (c Column) FrameCount() int { return MaxFrameCount(c.Children) } + +func (c *Column) UnmarshalJSON(data []byte) error { + type Alias Column + aux := &struct { + Children []json.RawMessage + *Alias + }{ + Alias: (*Alias)(c), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + children := []Widget{} + for _, childData := range aux.Children { + child, err := UnmarshalWidgetJSON(childData) + if err != nil { + return err + } + children = append(children, child) + } + c.Children = children + return nil +} diff --git a/render/image.go b/render/image.go index 7e6536471b..8c578d1568 100644 --- a/render/image.go +++ b/render/image.go @@ -2,6 +2,8 @@ package render import ( "bytes" + "encoding/base64" + "encoding/json" "errors" "fmt" "image" @@ -36,7 +38,8 @@ import ( // DOC(Height): Scale image to this height // DOC(Delay): (Read-only) Frame delay in ms, for animated GIFs type Image struct { - Widget + Type string `starlark:"-"` + Src string `starlark:"src,required"` Width, Height int Delay int `starlark:"delay,readonly"` @@ -195,3 +198,37 @@ func (p *Image) Init() error { return nil } + +func (p *Image) UnmarshalJSON(data []byte) error { + type Alias Image + aux := &struct { + *Alias + }{ + Alias: (*Alias)(p), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + src, err := base64.StdEncoding.DecodeString(aux.Src) + if err != nil { + return err + } + p.Src = string(src) + + return p.Init() +} + +func (p *Image) MarshalJSON() ([]byte, error) { + type Alias Image + aux := &struct { + *Alias + }{ + Alias: (*Alias)(p), + } + + aux.Src = base64.StdEncoding.EncodeToString([]byte(p.Src)) + + return json.Marshal(aux) +} diff --git a/render/marquee.go b/render/marquee.go index b7ec703ae0..88a4a1c937 100644 --- a/render/marquee.go +++ b/render/marquee.go @@ -1,6 +1,7 @@ package render import ( + "encoding/json" "image" "github.com/tidbyt/gg" @@ -45,7 +46,8 @@ import ( // ) // EXAMPLE END type Marquee struct { - Widget + Type string `starlark:"-"` + Child Widget `starlark:"child,required"` Width int `starlark:"width"` Height int `starlark:"height"` @@ -193,3 +195,22 @@ func (m Marquee) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { func (m Marquee) isVertical() bool { return m.ScrollDirection == "vertical" } + +func (m *Marquee) UnmarshalJSON(data []byte) error { + type Alias Marquee + aux := &struct { + Child json.RawMessage + *Alias + }{ + Alias: (*Alias)(m), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + child, err := UnmarshalWidgetJSON(aux.Child) + if err != nil { + return err + } + m.Child = child + return nil +} diff --git a/render/padding.go b/render/padding.go index 353ede3ce7..d8a9fbf92d 100644 --- a/render/padding.go +++ b/render/padding.go @@ -26,12 +26,12 @@ type Insets struct { // DOC(Pad): Padding around the child // DOC(Color): Background color type Padding struct { - Widget + Type string `starlark:"-"` Child Widget `starlark:"child,required"` Pad Insets Expanded bool - Color color.Color + Color color.RGBA } func (p Padding) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle { @@ -67,7 +67,7 @@ func (p Padding) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { height = cb.Dy() + p.Pad.Top + p.Pad.Bottom } - if p.Color != nil { + if p.Color != (color.RGBA{}) { dc.SetColor(p.Color) dc.DrawRectangle(0, 0, float64(width), float64(height)) dc.Fill() diff --git a/render/pie_chart.go b/render/pie_chart.go index 7c5989416d..58f10e85fd 100644 --- a/render/pie_chart.go +++ b/render/pie_chart.go @@ -1,6 +1,8 @@ package render import ( + "encoding/json" + "fmt" "image" "image/color" "math" @@ -24,11 +26,11 @@ import ( // ) // EXAMPLE END type PieChart struct { - Widget + Type string `starlark:"-"` - Colors []color.Color `starlark:"colors, required"` - Weights []float64 `starlark:"weights, required"` - Diameter int `starlark:"diameter,required"` + Colors []color.RGBA `starlark:"colors, required"` + Weights []float64 `starlark:"weights, required"` + Diameter int `starlark:"diameter,required"` } func (c PieChart) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle { @@ -58,3 +60,46 @@ func (c PieChart) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { func (c PieChart) FrameCount() int { return 1 } + +func (c *PieChart) MarshalJSON() ([]byte, error) { + type Alias PieChart + aux := &struct { + *Alias + Colors []string + }{ + Alias: (*Alias)(c), + Colors: make([]string, len(c.Colors)), + } + + for i, col := range c.Colors { + r, g, b, a := col.RGBA() + aux.Colors[i] = fmt.Sprintf("#%02x%02x%02x%02x", r>>8, g>>8, b>>8, a>>8) + } + + return json.Marshal(aux) +} + +func (c *PieChart) UnmarshalJSON(data []byte) error { + type Alias PieChart + aux := &struct { + *Alias + Colors []string + }{ + Alias: (*Alias)(c), + } + + err := json.Unmarshal(data, &aux) + if err != nil { + return err + } + + c.Colors = make([]color.RGBA, len(aux.Colors)) + for i, col := range aux.Colors { + c.Colors[i], err = ParseColor(col) + if err != nil { + return err + } + } + + return nil +} diff --git a/render/plot.go b/render/plot.go index 3fc6c701f2..42b5afaa82 100644 --- a/render/plot.go +++ b/render/plot.go @@ -1,6 +1,8 @@ package render import ( + "encoding/json" + "fmt" "image" "image/color" "math" @@ -51,7 +53,7 @@ var FillDampFactor uint8 = 0x55 // ), // EXAMPLE END type Plot struct { - Widget + Type string `starlark:"-"` // Coordinates of points to plot Data [][2]float64 `starlark:"data,required"` @@ -61,10 +63,10 @@ type Plot struct { Height int `starlark:"height,required"` // Primary line color - Color color.Color `starlark:"color"` + Color color.RGBA `starlark:"color"` // Optional line color for Y-values below 0 - ColorInverted color.Color `starlark:"color_inverted"` + ColorInverted color.RGBA `starlark:"color_inverted"` // Optional limit on X and Y axis XLim [2]float64 `starlark:"x_lim"` @@ -77,10 +79,10 @@ type Plot struct { ChartType string `starlark:"chart_type"` // Optional fill color for Y-values above 0 - FillColor color.Color `starlark:"fill_color"` + FillColor color.RGBA `starlark:"fill_color"` // Optional fill color for Y-values below 0 - FillColorInverted color.Color `starlark:"fill_color_inverted"` + FillColorInverted color.RGBA `starlark:"fill_color_inverted"` invThreshold int } @@ -187,7 +189,7 @@ func (p *Plot) translatePoints() []PathPoint { return points } -func dampenColor(c color.Color, a uint8) color.Color { +func dampenColor(c color.RGBA, a uint8) color.RGBA { r, g, b, _ := c.RGBA() return color.RGBA{uint8(r * uint32(a) / 255), uint8(g * uint32(a) / 255), uint8(b * uint32(a) / 255), 0xFF} } @@ -198,23 +200,22 @@ func (p Plot) PaintBounds(bounds image.Rectangle, frameIdx int) image.Rectangle func (p Plot) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { // Set line and fill colors - var col color.Color - col = color.RGBA{0xff, 0xff, 0xff, 0xff} - if p.Color != nil { + col := color.RGBA{0xff, 0xff, 0xff, 0xff} + if p.Color != (color.RGBA{}) { col = p.Color } colInv := col - if p.ColorInverted != nil { + if p.ColorInverted != (color.RGBA{}) { colInv = p.ColorInverted } fillCol := dampenColor(col, FillDampFactor) - if p.FillColor != nil { + if p.FillColor != (color.RGBA{}) { fillCol = p.FillColor } fillColInv := dampenColor(colInv, FillDampFactor) - if p.FillColorInverted != nil { + if p.FillColorInverted != (color.RGBA{}) { fillColInv = p.FillColorInverted } @@ -270,3 +271,86 @@ func (p Plot) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { func (p Plot) FrameCount() int { return 1 } + +func (p *Plot) MarshalJSON() ([]byte, error) { + type Alias Plot + aux := &struct { + *Alias + Color string + ColorInverted string + FillColor string + FillColorInverted string + }{ + Alias: (*Alias)(p), + } + + if p.Color != (color.RGBA{}) { + r, g, b, a := p.Color.RGBA() + aux.Color = fmt.Sprintf("#%02x%02x%02x%02x", r>>8, g>>8, b>>8, a>>8) + } + + if p.ColorInverted != (color.RGBA{}) { + r, g, b, a := p.ColorInverted.RGBA() + aux.ColorInverted = fmt.Sprintf("#%02x%02x%02x%02x", r>>8, g>>8, b>>8, a>>8) + } + + if p.FillColor != (color.RGBA{}) { + r, g, b, a := p.FillColor.RGBA() + aux.FillColor = fmt.Sprintf("#%02x%02x%02x%02x", r>>8, g>>8, b>>8, a>>8) + } + + if p.FillColorInverted != (color.RGBA{}) { + r, g, b, a := p.FillColorInverted.RGBA() + aux.FillColorInverted = fmt.Sprintf("#%02x%02x%02x%02x", r>>8, g>>8, b>>8, a>>8) + } + + return json.Marshal(aux) +} + +func (p *Plot) UnmarshalJSON(data []byte) error { + type Alias Plot + aux := &struct { + *Alias + Color string + ColorInverted string + FillColor string + FillColorInverted string + }{ + Alias: (*Alias)(p), + } + + err := json.Unmarshal(data, &aux) + if err != nil { + return err + } + + if aux.Color != "" { + p.Color, err = ParseColor(aux.Color) + if err != nil { + return err + } + } + + if aux.ColorInverted != "" { + p.ColorInverted, err = ParseColor(aux.ColorInverted) + if err != nil { + return err + } + } + + if aux.FillColor != "" { + p.FillColor, err = ParseColor(aux.FillColor) + if err != nil { + return err + } + } + + if aux.FillColorInverted != "" { + p.FillColorInverted, err = ParseColor(aux.FillColorInverted) + if err != nil { + return err + } + } + + return nil +} diff --git a/render/plot_test.go b/render/plot_test.go index 58aa8484a7..93e6291d55 100644 --- a/render/plot_test.go +++ b/render/plot_test.go @@ -367,8 +367,8 @@ func TestPlotInvertedColor(t *testing.T) { }, XLim: Empty, YLim: Empty, - Color: &color.RGBA{0, 0xff, 0, 0xff}, - ColorInverted: &color.RGBA{0xff, 0, 0, 0xff}, + Color: color.RGBA{0, 0xff, 0, 0xff}, + ColorInverted: color.RGBA{0xff, 0, 0, 0xff}, } assert.Equal(t, nil, ic.Check([]string{ "11....11..", @@ -419,7 +419,7 @@ func TestPlotSurfaceFill(t *testing.T) { }, XLim: Empty, YLim: Empty, - Color: &color.RGBA{0, 0xff, 0, 0xff}, + Color: color.RGBA{0, 0xff, 0, 0xff}, Fill: true, } assert.Equal(t, nil, ic.Check([]string{ @@ -436,7 +436,7 @@ func TestPlotSurfaceFill(t *testing.T) { }, PaintWidget(p, image.Rect(0, 0, 100, 100), 0))) // Fil with ColorInverted - p.ColorInverted = &color.RGBA{0xff, 0, 0, 0xff} + p.ColorInverted = color.RGBA{0xff, 0, 0, 0xff} assert.Equal(t, nil, ic.Check([]string{ "111111..............", ",,,,,,1.............", diff --git a/render/root.go b/render/root.go index b45ec545f4..e7500511e1 100644 --- a/render/root.go +++ b/render/root.go @@ -1,6 +1,7 @@ package render import ( + "encoding/json" "image" "image/color" "runtime" @@ -142,3 +143,23 @@ func PaintRoots(solidBackground bool, roots ...Root) []image.Image { return images } + +func (r *Root) UnmarshalJSON(data []byte) error { + type Alias Root + aux := &struct { + Child json.RawMessage + *Alias + }{ + Alias: (*Alias)(r), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + child, err := UnmarshalWidgetJSON(aux.Child) + if err != nil { + return err + } + r.Child = child + return nil +} diff --git a/render/row.go b/render/row.go index 15ec672a6b..d6d4bee68e 100644 --- a/render/row.go +++ b/render/row.go @@ -1,6 +1,7 @@ package render import ( + "encoding/json" "image" "github.com/tidbyt/gg" @@ -56,7 +57,7 @@ import ( // ) // EXAMPLE END type Row struct { - Widget + Type string `starlark:"-"` Children []Widget `starlark:"children,required"` MainAlign string `starlark:"main_align"` @@ -89,3 +90,26 @@ func (r Row) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { func (r Row) FrameCount() int { return MaxFrameCount(r.Children) } + +func (r *Row) UnmarshalJSON(data []byte) error { + type Alias Row + aux := &struct { + Children []json.RawMessage + *Alias + }{ + Alias: (*Alias)(r), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + children := []Widget{} + for _, childData := range aux.Children { + child, err := UnmarshalWidgetJSON(childData) + if err != nil { + return err + } + children = append(children, child) + } + r.Children = children + return nil +} diff --git a/render/sequence.go b/render/sequence.go index 3fb906c3a0..b2848e8224 100644 --- a/render/sequence.go +++ b/render/sequence.go @@ -28,7 +28,7 @@ import ( // ), // EXAMPLE END type Sequence struct { - Widget + Type string `starlark:"-"` Children []Widget `starlark:"children,required"` } diff --git a/render/sequence_test.go b/render/sequence_test.go index 8d37bd0c84..a8f8b45760 100644 --- a/render/sequence_test.go +++ b/render/sequence_test.go @@ -45,7 +45,7 @@ func TestSequenceOnlyOneFrameAtATime(t *testing.T) { func TestSequenceWithAnimatedChildren(t *testing.T) { // Returns a 2x2 grid with background color, and a single // pixel of foreground color at x,y. - frame := func(x, y int, fg color.Color, bg color.Color) Widget { + frame := func(x, y int, fg color.RGBA, bg color.RGBA) Widget { row0 := Row{ Children: []Widget{ Box{Width: 1, Height: 1, Color: bg}, diff --git a/render/stack.go b/render/stack.go index 397c7c25ee..565b2c314c 100644 --- a/render/stack.go +++ b/render/stack.go @@ -26,7 +26,8 @@ import ( // ) // EXAMPLE END type Stack struct { - Widget + Type string `starlark:"-"` + Children []Widget `starlark:"children,required"` } diff --git a/render/starfield.go b/render/starfield.go index bd11f42053..b2b75c6962 100644 --- a/render/starfield.go +++ b/render/starfield.go @@ -14,7 +14,7 @@ type Starfield struct { Widget Child Widget - Color color.Color + Color color.RGBA Width int Height int stars []*Star diff --git a/render/text.go b/render/text.go index eb5d37e20c..cafa867114 100644 --- a/render/text.go +++ b/render/text.go @@ -1,6 +1,8 @@ package render import ( + "encoding/json" + "fmt" "image" "image/color" @@ -31,12 +33,13 @@ var ( // render.Text(content="Tidbyt!", color="#099") // EXAMPLE END type Text struct { - Widget + Type string `starlark:"-"` + Content string `starlark:"content,required"` Font string Height int Offset int - Color color.Color + Color color.RGBA img image.Image } @@ -85,7 +88,7 @@ func (t *Text) Init() error { dc = gg.NewContext(width, height) dc.SetFontFace(face) - if t.Color != nil { + if t.Color != (color.RGBA{}) { dc.SetColor(t.Color) } else { dc.SetColor(DefaultFontColor) @@ -101,3 +104,45 @@ func (t *Text) Init() error { func (t Text) FrameCount() int { return 1 } + +func (t *Text) UnmarshalJSON(data []byte) error { + type Alias Text + aux := &struct { + *Alias + Color string + }{ + Alias: (*Alias)(t), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if aux.Color != "" { + col, err := ParseColor(aux.Color) + if err != nil { + return err + } + t.Color = col + } + + return t.Init() +} + +func (t *Text) MarshalJSON() ([]byte, error) { + col := "" + if t.Color != (color.RGBA{}) { + r, g, b, a := t.Color.RGBA() + col = fmt.Sprintf("#%02x%02x%02x%02x", r>>8, g>>8, b>>8, a>>8) + } + type Alias Text + aux := &struct { + *Alias + Color string + }{ + Alias: (*Alias)(t), + Color: col, + } + + return json.Marshal(aux) +} diff --git a/render/tracer.go b/render/tracer.go index b4bdb7728a..6ffad51b6b 100644 --- a/render/tracer.go +++ b/render/tracer.go @@ -8,7 +8,8 @@ import ( ) type Tracer struct { - Widget + Type string `starlark:"-"` + Path Path TraceLength int } diff --git a/render/vector.go b/render/vector.go index 23539d19de..a0fb02ac74 100644 --- a/render/vector.go +++ b/render/vector.go @@ -1,6 +1,7 @@ package render import ( + "encoding/json" "image" "github.com/tidbyt/gg" @@ -14,7 +15,7 @@ import ( // column). MainAlign controls how children are placed along this // axis. CrossAlign controls placement orthogonally to the main axis. type Vector struct { - Widget + Type string `starlark:"-"` Children []Widget MainAlign string `starlark: "main_align"` @@ -238,3 +239,26 @@ func (v Vector) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx int) { func (v Vector) FrameCount() int { return MaxFrameCount(v.Children) } + +func (v *Vector) UnmarshalJSON(data []byte) error { + type Alias Vector + aux := &struct { + Children []json.RawMessage + *Alias + }{ + Alias: (*Alias)(v), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + children := []Widget{} + for _, childData := range aux.Children { + child, err := UnmarshalWidgetJSON(childData) + if err != nil { + return err + } + children = append(children, child) + } + v.Children = children + return nil +} diff --git a/render/widget.go b/render/widget.go index a8194e69b1..25ce1fbd11 100644 --- a/render/widget.go +++ b/render/widget.go @@ -1,6 +1,8 @@ package render import ( + "encoding/json" + "fmt" "image" "github.com/tidbyt/gg" @@ -46,3 +48,45 @@ func MaxFrameCount(widgets []Widget) int { return m } + +func UnmarshalWidgetJSON(data []byte) (Widget, error) { + if string(data) == "null" { + return nil, nil + } + + var widgetType struct { + Type string `json:"type"` + } + if err := json.Unmarshal(data, &widgetType); err != nil { + return nil, err + } + + var widget Widget + switch widgetType.Type { + case "Animation": + widget = &Animation{} + case "Box": + widget = &Box{} + case "Column": + widget = &Column{} + case "Image": + widget = &Image{} + case "Marquee": + widget = &Marquee{} + case "Row": + widget = &Row{} + case "Text": + widget = &Text{} + case "Vector": + widget = &Vector{} + + default: + return nil, fmt.Errorf("unknown widget type: %s", widgetType.Type) + } + + if err := json.Unmarshal(data, widget); err != nil { + return nil, err + } + + return widget, nil +} diff --git a/render/wrappedtext.go b/render/wrappedtext.go index 495ea8602b..252ff7e859 100644 --- a/render/wrappedtext.go +++ b/render/wrappedtext.go @@ -37,14 +37,14 @@ import ( // ) // EXAMPLE END type WrappedText struct { - Widget + Type string `starlark:"-"` Content string `starlark:"content,required"` Font string Height int Width int LineSpacing int - Color color.Color + Color color.RGBA Align string face font.Face @@ -130,7 +130,7 @@ func (tw *WrappedText) Paint(dc *gg.Context, bounds image.Rectangle, frameIdx in descent := metrics.Descent.Floor() dc.SetFontFace(tw.face) - if tw.Color != nil { + if tw.Color != (color.RGBA{}) { dc.SetColor(tw.Color) } else { dc.SetColor(DefaultFontColor) diff --git a/runtime/gen/main.go b/runtime/gen/main.go index 19450ca573..bf804923e3 100644 --- a/runtime/gen/main.go +++ b/runtime/gen/main.go @@ -162,7 +162,7 @@ var TypeMap = map[reflect.Type]Type{ DocType: "[Widget]", TemplatePath: "./runtime/gen/attr/children.tmpl", }, - toDecayedType(new(color.Color)): { + toDecayedType(new(color.RGBA)): { GoType: "starlark.String", DocType: `color`, TemplatePath: "./runtime/gen/attr/color.tmpl", @@ -170,7 +170,7 @@ var TypeMap = map[reflect.Type]Type{ }, // Render `PieChart types` - toDecayedType(new([]color.Color)): { + toDecayedType(new([]color.RGBA)): { GoType: "*starlark.List", DocType: `[color]`, TemplatePath: "./runtime/gen/attr/colors.tmpl", @@ -320,6 +320,9 @@ func toGeneratedAttribute(typ reflect.Type, field reflect.StructField) (*Generat } result.StarlarkName = strings.TrimSpace(attrs[0]) + if result.StarlarkName == "-" { + return nil, nil + } for _, attr := range attrs[1:] { attr = strings.TrimSpace(attr) @@ -378,7 +381,8 @@ func toGeneratedType(pkg Package, val reflect.Value) (*GeneratedType, error) { continue } - if attr, err := toGeneratedAttribute(typ, field); err == nil { + attr, err := toGeneratedAttribute(typ, field) + if attr != nil && err == nil { result.Attributes = append(result.Attributes, attr) if t, ok := TypeMap[field.Type]; ok { @@ -390,7 +394,7 @@ func toGeneratedType(pkg Package, val reflect.Value) (*GeneratedType, error) { } else { return nil, fmt.Errorf("%s.%s has unsupported type", typ.Name(), field.Name) } - } else { + } else if err != nil { return nil, err } } diff --git a/runtime/gen/type.tmpl b/runtime/gen/type.tmpl index 77ae9638c6..b4fed7e38f 100644 --- a/runtime/gen/type.tmpl +++ b/runtime/gen/type.tmpl @@ -68,6 +68,7 @@ func (w *{{.GoName}}) AsRenderRoot() render.Root { {{if .GoWidgetName }} func (w *{{.GoName}}) AsRenderWidget() render.Widget { + w.{{.GoName}}.Type = "{{.GoName}}" return &w.{{.GoName}} } {{end}} diff --git a/runtime/modules/animation_runtime/generated.go b/runtime/modules/animation_runtime/generated.go index aa7311b568..43782f2433 100644 --- a/runtime/modules/animation_runtime/generated.go +++ b/runtime/modules/animation_runtime/generated.go @@ -140,6 +140,7 @@ func newAnimatedPositioned( } func (w *AnimatedPositioned) AsRenderWidget() render.Widget { + w.AnimatedPositioned.Type = "AnimatedPositioned" return &w.AnimatedPositioned } @@ -702,6 +703,7 @@ func newTransformation( } func (w *Transformation) AsRenderWidget() render.Widget { + w.Transformation.Type = "Transformation" return &w.Transformation } diff --git a/runtime/modules/render_runtime/data.go b/runtime/modules/render_runtime/data.go index bf164c8de0..a6458e41a1 100644 --- a/runtime/modules/render_runtime/data.go +++ b/runtime/modules/render_runtime/data.go @@ -71,8 +71,8 @@ func WeightsFromStarlark(list *starlark.List) ([]float64, error) { return result, nil } -func ColorSeriesFromStarlark(list *starlark.List) ([]color.Color, error) { - result := make([]color.Color, 0) +func ColorSeriesFromStarlark(list *starlark.List) ([]color.RGBA, error) { + result := make([]color.RGBA, 0) for i := 0; i < list.Len(); i++ { c := list.Index(i) diff --git a/runtime/modules/render_runtime/generated.go b/runtime/modules/render_runtime/generated.go index 8c220b3595..ed493c6753 100644 --- a/runtime/modules/render_runtime/generated.go +++ b/runtime/modules/render_runtime/generated.go @@ -137,6 +137,7 @@ func newAnimation( } func (w *Animation) AsRenderWidget() render.Widget { + w.Animation.Type = "Animation" return &w.Animation } @@ -257,6 +258,7 @@ func newBox( } func (w *Box) AsRenderWidget() render.Widget { + w.Box.Type = "Box" return &w.Box } @@ -385,6 +387,7 @@ func newCircle( } func (w *Circle) AsRenderWidget() render.Widget { + w.Circle.Type = "Circle" return &w.Circle } @@ -509,6 +512,7 @@ func newColumn( } func (w *Column) AsRenderWidget() render.Widget { + w.Column.Type = "Column" return &w.Column } @@ -620,6 +624,7 @@ func newImage( } func (w *Image) AsRenderWidget() render.Widget { + w.Image.Type = "Image" return &w.Image } @@ -773,6 +778,7 @@ func newMarquee( } func (w *Marquee) AsRenderWidget() render.Widget { + w.Marquee.Type = "Marquee" return &w.Marquee } @@ -949,6 +955,7 @@ func newPadding( } func (w *Padding) AsRenderWidget() render.Widget { + w.Padding.Type = "Padding" return &w.Padding } @@ -1066,6 +1073,7 @@ func newPieChart( } func (w *PieChart) AsRenderWidget() render.Widget { + w.PieChart.Type = "PieChart" return &w.PieChart } @@ -1254,6 +1262,7 @@ func newPlot( } func (w *Plot) AsRenderWidget() render.Widget { + w.Plot.Type = "Plot" return &w.Plot } @@ -1516,6 +1525,7 @@ func newRow( } func (w *Row) AsRenderWidget() render.Widget { + w.Row.Type = "Row" return &w.Row } @@ -1632,6 +1642,7 @@ func newSequence( } func (w *Sequence) AsRenderWidget() render.Widget { + w.Sequence.Type = "Sequence" return &w.Sequence } @@ -1736,6 +1747,7 @@ func newStack( } func (w *Stack) AsRenderWidget() render.Widget { + w.Stack.Type = "Stack" return &w.Stack } @@ -1852,6 +1864,7 @@ func newText( } func (w *Text) AsRenderWidget() render.Widget { + w.Text.Type = "Text" return &w.Text } @@ -2006,6 +2019,7 @@ func newWrappedText( } func (w *WrappedText) AsRenderWidget() render.Widget { + w.WrappedText.Type = "WrappedText" return &w.WrappedText }