172 lines
4.0 KiB
Go
172 lines
4.0 KiB
Go
package mujica
|
||
|
||
import (
|
||
"fmt"
|
||
"log/slog"
|
||
"math"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
|
||
"mujicaui/icons"
|
||
"mujicaui/theme"
|
||
)
|
||
|
||
// Trend picks the rise color: green, or red (China).
|
||
type Trend int
|
||
|
||
const (
|
||
GreenUp Trend = iota
|
||
RedUp
|
||
)
|
||
|
||
type trendKey struct{}
|
||
|
||
// UseTrend sets the window's rise and fall colors.
|
||
func UseTrend(c *ui.Context, t Trend) {
|
||
if t != GreenUp && t != RedUp {
|
||
panic(fmt.Sprintf("mujica: unknown Trend %d", t))
|
||
}
|
||
*ui.Local(c.Root(), trendKey{}, func() Trend { return GreenUp }) = t
|
||
}
|
||
|
||
// trendColor colors a rise (sign > 0), fall or flat.
|
||
func trendColor(c *ui.Context, sign int) ui.Color {
|
||
k := Tokens(c)
|
||
up, down := k.Success, k.Danger
|
||
if *ui.Local(c.Root(), trendKey{}, func() Trend { return GreenUp }) == RedUp {
|
||
up, down = k.Danger, k.Success
|
||
}
|
||
switch {
|
||
case sign > 0:
|
||
return up
|
||
case sign < 0:
|
||
return down
|
||
}
|
||
return k.TextMuted
|
||
}
|
||
|
||
// trendIcon is the arrow that says the direction without color.
|
||
func trendIcon(sign int) *ui.SVG {
|
||
switch {
|
||
case sign > 0:
|
||
return icons.Must("arrow-up")
|
||
case sign < 0:
|
||
return icons.Must("arrow-down")
|
||
}
|
||
return icons.Must("minus")
|
||
}
|
||
|
||
// trendWord names the direction for assistive technology.
|
||
func trendWord(c *ui.Context, sign int) string {
|
||
switch {
|
||
case sign > 0:
|
||
return msg(c, "price_text.up")
|
||
case sign < 0:
|
||
return msg(c, "price_text.down")
|
||
}
|
||
return msg(c, "price_text.flat")
|
||
}
|
||
|
||
func trendSign(v float64) int {
|
||
switch {
|
||
case v > 0:
|
||
return 1
|
||
case v < 0:
|
||
return -1
|
||
}
|
||
return 0
|
||
}
|
||
|
||
// priceDecimals resolves a Decimals option: zero means 2, negative panics.
|
||
func priceDecimals(name string, d int) int {
|
||
if d < 0 {
|
||
panic("mujica: " + name + " with negative Decimals")
|
||
}
|
||
if d == 0 {
|
||
return 2
|
||
}
|
||
return d
|
||
}
|
||
|
||
// priceTextFlash is the tint's fade, three state steps long.
|
||
const priceTextFlash = 3 * theme.StateDuration
|
||
|
||
// PriceTextOptions configure a PriceText.
|
||
type PriceTextOptions struct {
|
||
// Decimals shown; zero means 2.
|
||
Decimals int
|
||
// NoGrouping drops the thousands separators.
|
||
NoGrouping bool
|
||
// Compare marks the price's move from Reference.
|
||
Compare bool
|
||
Reference float64
|
||
// NoFlash keeps the price still when it changes.
|
||
NoFlash bool
|
||
// Size is the font size; zero is the body size.
|
||
Size float32
|
||
}
|
||
|
||
type priceTextState struct {
|
||
last float64
|
||
seen bool
|
||
at time.Time
|
||
dir int
|
||
}
|
||
|
||
// PriceText shows a price; a change tints it once.
|
||
func PriceText(c *ui.Context, value float64, opts PriceTextOptions) *ui.Element {
|
||
dec := priceDecimals("PriceText", opts.Decimals)
|
||
if opts.Size < 0 {
|
||
panic("mujica: PriceText with negative Size")
|
||
}
|
||
size := opts.Size
|
||
if size == 0 {
|
||
size = theme.BodySize
|
||
}
|
||
text := statisticFormat(value, dec)
|
||
if opts.NoGrouping {
|
||
text = priceUngrouped(value, dec)
|
||
}
|
||
e := ui.Row(c).Gap(Density(c).Unit()).AlignItems(ui.Center).Padding(0, 2).Radius(theme.ControlRadius).Shrink(0)
|
||
st := ui.Local(e, "price", func() priceTextState { return priceTextState{} })
|
||
if st.seen && value != st.last {
|
||
st.at, st.dir = c.Now(), trendSign(value-st.last)
|
||
slog.Debug("mujica: price_text changed", "dir", st.dir)
|
||
}
|
||
st.last, st.seen = value, true
|
||
sign := 0
|
||
if opts.Compare {
|
||
sign = trendSign(value - opts.Reference)
|
||
}
|
||
col := Tokens(c).Text
|
||
spoken := text
|
||
if opts.Compare {
|
||
col = trendColor(c, sign)
|
||
spoken += ", " + trendWord(c, sign)
|
||
}
|
||
if d := Motion(c, priceTextFlash); d > 0 && !opts.NoFlash && !st.at.IsZero() {
|
||
if t := c.Now().Sub(st.at); t < d {
|
||
e.Background(trendColor(c, st.dir).Alpha(0.2 * float32(1-float64(t)/float64(d))))
|
||
c.AnimationFrame()
|
||
}
|
||
}
|
||
e.Role(ui.RoleText).Label(spoken).TextColor(col)
|
||
e.Children(func() {
|
||
if opts.Compare {
|
||
ui.Icon(c, trendIcon(sign)).Size(size*0.85, size*0.85)
|
||
}
|
||
ui.Text(c, text).FontSize(FontSize(c, size)).FontFeatures("tnum").FontWeight(500).SingleLine()
|
||
})
|
||
return e
|
||
}
|
||
|
||
// priceUngrouped writes v with dec decimals and no separators.
|
||
func priceUngrouped(v float64, dec int) string {
|
||
s := fmt.Sprintf("%.*f", dec, math.Abs(v))
|
||
if v < 0 && s != fmt.Sprintf("%.*f", dec, 0.0) {
|
||
return "−" + s
|
||
}
|
||
return s
|
||
}
|