Files
MujicaUI/gauge.go
T

155 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mujica
import (
"fmt"
"math"
"github.com/egoist/mygo/ui"
"mujicaui/internal"
"mujicaui/theme"
)
// GaugeTone colors a Gauge segment; its label still names it.
type GaugeTone int
const (
GaugeNeutral GaugeTone = iota
GaugeGood
GaugeWarning
GaugeDanger
)
// GaugeSegment is a named dial range up to To.
type GaugeSegment struct {
To float64
Label string
Tone GaugeTone
}
// GaugeOptions configure a Gauge; Frame.Height 0 means 200 DIPs.
type GaugeOptions struct {
Frame ChartFrame
Min, Max float64
// Segments split Min..Max ascending; the last ends at Max.
Segments []GaugeSegment
Unit string
// Full draws a whole circle instead of a half.
Full bool
Format func(v float64) string
}
// Gauge points a needle at value on named segments.
func Gauge(c *ui.Context, value float64, opts GaugeOptions) ChartResult {
if !(opts.Max > opts.Min) || math.IsNaN(value) || value < opts.Min || value > opts.Max {
panic("mujica: Gauge needs Min < Max and a value between them")
}
prev := opts.Min
for i, s := range opts.Segments {
if s.To <= prev || s.Label == "" || i == len(opts.Segments)-1 && s.To != opts.Max {
panic("mujica: Gauge segments need labels and ascending ends finishing at Max")
}
prev = s.To
}
if opts.Frame.Height == 0 {
opts.Frame.Height = 200
}
ch := newChart(c, opts.Frame)
k := Tokens(c)
size := FontSize(c, theme.CaptionSize)
tone := func(t GaugeTone) ui.Color {
return map[GaugeTone]ui.Color{GaugeNeutral: internal.Palette(k)[5], GaugeGood: k.Success, GaugeWarning: k.Warning, GaugeDanger: k.Danger}[t]
}
val := func(v float64) string { return chartValue(c, v, opts.Format) }
seg := -1
for i, s := range opts.Segments {
if value <= s.To && seg < 0 {
seg = i
}
}
a0, a1 := -math.Pi/2, math.Pi/2
if opts.Full {
a0, a1 = -math.Pi, math.Pi
}
angle := func(v float64) float64 { return a0 + (a1-a0)*(v-opts.Min)/(opts.Max-opts.Min) }
geom := func(r ui.Rect) (cx, cy, rad float32) {
if opts.Full {
rad = min(r.W, r.H)/2 - 12
return r.X + r.W/2, r.Y + r.H/2, rad
}
rad = min(r.W/2-12, r.H-36)
return r.X + r.W/2, r.Y + 8 + rad, rad
}
reading := val(value) + " " + opts.Unit
if seg >= 0 {
reading += " · " + opts.Segments[seg].Label
}
spec := chartSpec{n: 1 + len(opts.Segments)}
spec.readout = func(i int) string {
if i == 0 {
return reading
}
s, from := opts.Segments[i-1], opts.Min
if i > 1 {
from = opts.Segments[i-2].To
}
return fmt.Sprintf("%s: %s–%s", s.Label, val(from), val(s.To))
}
spec.pick = func(r ui.Rect, x, y float32) int {
cx, cy, rad := geom(r)
d := float32(math.Hypot(float64(x-cx), float64(y-cy)))
if d < rad*0.45 {
return 0
}
if d > rad+6 {
return -1
}
a := math.Atan2(float64(x-cx), float64(cy-y))
from := a0
for i, s := range opts.Segments {
if a >= from && a <= angle(s.To) {
return i + 1
}
from = angle(s.To)
}
return -1
}
spec.draw = func(p *ui.Painter, r ui.Rect, active int) {
cx, cy, rad := geom(r)
from := a0
if len(opts.Segments) == 0 {
p.FillPath(internal.Sector(cx, cy, rad*0.78, rad, a0, a1), k.SurfaceHover)
}
for i, s := range opts.Segments {
to := angle(s.To)
grow := float32(0)
if active == i+1 {
grow = 4
}
p.FillPath(internal.Sector(cx, cy, rad*0.78-grow, rad+grow, from, to), tone(s.Tone))
p.StrokePath(internal.Sector(cx, cy, rad*0.78-grow, rad+grow, from, to), 1.5, k.Surface)
lx, ly := internal.Polar(cx, cy, rad+10, to)
if i < len(opts.Segments)-1 {
chartText(p, val(s.To), lx, ly, size, k.TextMuted, 0.5, 1)
}
from = to
}
nx, ny := internal.Polar(cx, cy, rad*0.9, angle(value))
var needle ui.Path
needle.MoveTo(cx, cy).LineTo(internal.Snap(p, nx), internal.Snap(p, ny))
w := float32(3)
if active == 0 {
w = 4.5
}
p.StrokePath(&needle, w, k.Text)
p.FillPath(internal.Diamond(cx, cy, 12), k.Text)
ty := cy + 34
chartText(p, val(value)+" "+opts.Unit, cx, ty, FontSize(c, theme.H3Size), k.Text, 0.5, 1)
if seg >= 0 {
chartText(p, opts.Segments[seg].Label, cx, ty+2, size, k.TextMuted, 0.5, 0)
}
}
res, _ := ch.build(spec)
return res
}