Files
MujicaUI/ticker_tape.go
T

156 lines
4.4 KiB
Go

package mujica
import (
"fmt"
"log/slog"
"math"
"time"
"github.com/egoist/mygo/ui"
"mujicaui/icons"
"mujicaui/theme"
)
// TickerItem is one symbol on a TickerTape.
type TickerItem struct {
Symbol string
Price float64
ChangePercent float64
}
// TickerTapeOptions configure a TickerTape.
type TickerTapeOptions struct {
// Decimals of the prices; zero means 2.
Decimals int
// Speed is in DIPs a second; zero means 40.
Speed float32
// PerPage is items per reduced-motion page; zero means 4.
PerPage int
// Label names the tape; empty uses the built-in "Market ticker".
Label string
}
type tickerTapeState struct {
offset float32
last time.Time
page int
paused bool
}
// TickerTape loops quotes, pausing on hover or focus.
func TickerTape(c *ui.Context, items []TickerItem, opts TickerTapeOptions) *ui.Element {
if opts.Speed < 0 || opts.PerPage < 0 {
panic("mujica: TickerTape Speed and PerPage cannot be negative")
}
dec := priceDecimals("TickerTape", opts.Decimals)
speed, per := opts.Speed, opts.PerPage
if speed == 0 {
speed = 40
}
if per == 0 {
per = 4
}
label := opts.Label
if label == "" {
label = msg(c, "ticker_tape.label")
}
k := Tokens(c)
d := Density(c)
e := ui.Row(c).MinHeight(d.ControlHeight()).AlignItems(ui.Center).Background(k.Surface).
BorderWidth(theme.BorderWidth, 0).BorderColor(k.Border).Focusable().Role(ui.RoleGroup).Label(label)
e.FocusRing(false)
st := ui.Local(e, "tape", func() tickerTapeState { return tickerTapeState{} })
if c.Preferences().ReduceMotion || len(items) == 0 {
tickerTapePages(c, e, st, items, per, dec)
return e
}
paused := e.Hovered() || e.FocusWithin()
if paused != st.paused {
st.paused = paused
slog.Debug("mujica: ticker_tape paused", "paused", paused)
}
now := c.Now()
if !paused && !st.last.IsZero() {
st.offset += float32(now.Sub(st.last).Seconds()) * speed
}
st.last = now
if !paused {
c.AnimationFrame()
}
e.Description(msg(c, "ticker_tape.pause_hint"))
e.ClipX().Children(func() {
strip := ui.Row(c).Absolute().Top(0).Bottom(0).AlignItems(ui.Center)
if w := strip.Bounds().W / 2; w > 0 {
st.offset = float32(math.Mod(float64(st.offset), float64(w)))
}
// Quarter steps keep the moving text on the renderer's grid.
strip.Left(-float32(math.Round(float64(st.offset)*4)) / 4)
strip.Children(func() {
// The second copy fills in as the first scrolls out.
for n := range 2 {
r := ui.Row(c).Key(n).AlignItems(ui.Center).Shrink(0)
if n == 1 {
r.Role(ui.RoleNone)
}
r.Children(func() {
for _, it := range items {
tickerTapeItem(c, it, dec)
}
})
}
})
})
return e
}
// tickerTapePages shows one page of items, for reduced motion.
func tickerTapePages(c *ui.Context, e *ui.Element, st *tickerTapeState, items []TickerItem, per, dec int) {
pages := max(1, (len(items)+per-1)/per)
st.page = min(st.page, pages-1)
turn := func(by int) {
if !e.IsDisabled() {
st.page = (st.page + by + pages) % pages
slog.Debug("mujica: ticker_tape page", "page", st.page)
}
}
if e.Shortcut(0, ui.KeyRight) {
turn(1)
}
if e.Shortcut(0, ui.KeyLeft) {
turn(-1)
}
k := Tokens(c)
e.Children(func() {
ui.Row(c).Grow(1).MinWidth(0).ClipX().AlignItems(ui.Center).Children(func() {
for _, it := range items[st.page*per : min(len(items), (st.page+1)*per)] {
tickerTapeItem(c, it, dec)
}
})
if pages < 2 {
return
}
if iconAction(c, icons.Must("chevron-left"), msg(c, "ticker_tape.previous")).Clicked() {
turn(-1)
}
ui.Text(c, fmt.Sprintf("%d / %d", st.page+1, pages)).TextColor(k.TextMuted).FontSize(FontSize(c, theme.CaptionSize)).FontFeatures("tnum")
if iconAction(c, icons.Must("chevron-right"), msg(c, "ticker_tape.next")).Clicked() {
turn(1)
}
})
}
// tickerTapeItem is one symbol, price and change.
func tickerTapeItem(c *ui.Context, it TickerItem, dec int) {
u := Density(c).Unit()
sign := trendSign(it.ChangePercent)
ui.Row(c).Gap(u*1.5).AlignItems(ui.Center).Padding(0, u*4).Shrink(0).Children(func() {
ui.Text(c, it.Symbol).FontWeight(600).SingleLine()
ui.Text(c, statisticFormat(it.Price, dec)).FontFeatures("tnum").SingleLine()
ui.Row(c).Gap(u / 2).AlignItems(ui.Center).TextColor(trendColor(c, sign)).Label(trendWord(c, sign) + " " + statisticSigned(it.ChangePercent, 2) + "%").Children(func() {
ui.Icon(c, trendIcon(sign)).Size(12, 12)
ui.Text(c, statisticSigned(it.ChangePercent, 2)+"%").FontFeatures("tnum").SingleLine()
})
})
}