Split into category packages; module github.com/ZacharyZhang-NY/MujicaUI
This commit is contained in:
1822 files changed
+24568
-21314
No files matched your search
@@ -0,0 +1,241 @@
|
||||
package chart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/core"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/input"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/internal"
|
||||
"github.com/ZacharyZhang-NY/MujicaUI/theme"
|
||||
)
|
||||
|
||||
// ChartBrushOptions configure a ChartBrush.
|
||||
type ChartBrushOptions struct {
|
||||
// Label names the brush for assistive technology; required.
|
||||
Label string
|
||||
// Min and Max span the track; Min < Max.
|
||||
Min, Max float64
|
||||
// Step defaults to one hundredth of the span.
|
||||
Step float64
|
||||
// Values draws a preview line across the track.
|
||||
Values []float64
|
||||
// Height is the track height; 0 means 48 DIPs.
|
||||
Height float32
|
||||
Format func(v float64) string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
type brushDrag struct {
|
||||
part int
|
||||
grab float64
|
||||
start float64
|
||||
}
|
||||
|
||||
// ChartBrush selects a range with dragging and keyboard controls.
|
||||
func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) core.ChoiceResult {
|
||||
if opts.Label == "" || !(opts.Max > opts.Min) || math.IsInf(opts.Min, 0) || math.IsInf(opts.Max, 0) || math.IsNaN(opts.Step) || math.IsInf(opts.Step, 0) || opts.Step < 0 || math.IsNaN(float64(opts.Height)) || math.IsInf(float64(opts.Height), 0) || opts.Height != 0 && opts.Height < theme.MinHitSize {
|
||||
panic("mujica: ChartBrush needs a Label, finite domain and step, and Height ≥ 28")
|
||||
}
|
||||
step := opts.Step
|
||||
if step == 0 {
|
||||
step = (opts.Max - opts.Min) / 100
|
||||
}
|
||||
spec := input.NumberInputResolve(input.NumberInputOptions{Min: opts.Min, Max: opts.Max, Step: step})
|
||||
if value[0] < opts.Min || value[1] > opts.Max || !(value[1] > value[0]) {
|
||||
panic("mujica: ChartBrush value must be an ascending range inside Min..Max")
|
||||
}
|
||||
h := opts.Height
|
||||
if h == 0 {
|
||||
h = 48
|
||||
}
|
||||
k := core.Tokens(c)
|
||||
u := core.Density(c).Unit()
|
||||
val := func(v float64) string { return ChartValue(c, v, opts.Format) }
|
||||
snap := func(v float64) float64 {
|
||||
return max(spec.Lo, min(spec.Hi, spec.Round(spec.Lo+math.Round((v-spec.Lo)/spec.Step)*spec.Step)))
|
||||
}
|
||||
before := *value
|
||||
var res core.ChoiceResult
|
||||
res.Element = ui.Column(c).Gap(u).MinWidth(0).Disabled(opts.Disabled)
|
||||
res.Element.Children(func() {
|
||||
track := ui.Box(c).Height(h).FillWidth().MinWidth(0).Radius(theme.ControlRadius).Background(k.SurfaceHover)
|
||||
b := track.Bounds()
|
||||
internal.Remeasure(c, track, b.W, b.H)
|
||||
at := func(v float64) float32 { return float32((v-opts.Min)/(opts.Max-opts.Min)) * b.W }
|
||||
from := func(px float32) float64 { return opts.Min + float64(px/max(b.W, 1))*(opts.Max-opts.Min) }
|
||||
drag := ui.Local(track, "mujica.brush.drag", func() brushDrag { return brushDrag{part: -1} })
|
||||
set := func(lo, hi float64) {
|
||||
lo, hi = snap(lo), snap(hi)
|
||||
if hi-lo >= spec.Step-1e-9 {
|
||||
*value = [2]float64{lo, hi}
|
||||
}
|
||||
}
|
||||
move := func(lo, hi float64) {
|
||||
w := hi - lo
|
||||
lo = max(spec.Lo, min(lo, spec.Hi-w))
|
||||
set(lo, lo+w)
|
||||
}
|
||||
preview := ui.Local(track, "mujica.brush.preview", func() brushPreview { return brushPreview{} })
|
||||
if id := ChartID(opts.Values); preview.id != id {
|
||||
*preview = brushPreview{id: id, lo: math.Inf(1), hi: math.Inf(-1)}
|
||||
for _, v := range opts.Values {
|
||||
if math.IsInf(v, 0) {
|
||||
panic("mujica: ChartBrush preview values must be finite or NaN")
|
||||
}
|
||||
if !math.IsNaN(v) {
|
||||
preview.lo, preview.hi = min(preview.lo, v), max(preview.hi, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
track.Draw(func(p *ui.Painter, r ui.Rect) { brushPaint(p, r, *value, opts, k, preview) })
|
||||
track.Children(func() {
|
||||
x0, x1 := at(value[0]), at(value[1])
|
||||
end := func(i int, x float32) *ui.Element {
|
||||
e := ui.Box(c).Absolute().Left(x-theme.MinHitSize/2).Top(0).Size(theme.MinHitSize, h).Focusable().
|
||||
Role(ui.RoleSlider).Label(fmt.Sprintf(core.Msg(c, []string{"chart_brush.start", "chart_brush.end"}[i]), val(value[i])))
|
||||
internal.Ring(e, k.Focus, theme.ControlRadius)
|
||||
return e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
cx := internal.Snap(p, r.X+r.W/2)
|
||||
p.Fill(ui.Rect{X: cx - 3, Y: r.Y + r.H/2 - 10, W: 6, H: 20}, k.Text, 2)
|
||||
})
|
||||
}
|
||||
// Tab order runs start, window, end.
|
||||
ends := [2]*ui.Element{end(0, x0)}
|
||||
window := ui.Box(c).Absolute().Left(x0 + theme.MinHitSize/2).Top(0).Width(max(x1-x0-theme.MinHitSize, theme.MinHitSize)).Height(h).Focusable().
|
||||
Role(ui.RoleGroup).Label(fmt.Sprintf(core.Msg(c, "chart_brush.window"), opts.Label, val(value[0]), val(value[1])))
|
||||
internal.Ring(window, k.Focus, theme.ControlRadius)
|
||||
ends[1] = end(1, x1)
|
||||
if res.Element.IsDisabled() {
|
||||
drag.part = -1
|
||||
return
|
||||
}
|
||||
for i, end := range ends {
|
||||
v, ok := brushKey(end, value[i], spec)
|
||||
switch {
|
||||
case !ok:
|
||||
case i == 0:
|
||||
set(min(v, value[1]-spec.Step), value[1])
|
||||
default:
|
||||
set(value[0], max(v, value[0]+spec.Step))
|
||||
}
|
||||
}
|
||||
w := value[1] - value[0]
|
||||
switch {
|
||||
case window.Shortcut(0, ui.KeyLeft):
|
||||
lo := max(spec.Lo, spec.StepBy(value[0], -1))
|
||||
set(lo, lo+w)
|
||||
case window.Shortcut(0, ui.KeyRight):
|
||||
hi := min(spec.Hi, spec.StepBy(value[1], 1))
|
||||
set(hi-w, hi)
|
||||
case window.Shortcut(0, ui.KeyHome):
|
||||
set(spec.Lo, spec.Lo+w)
|
||||
case window.Shortcut(0, ui.KeyEnd):
|
||||
set(spec.Hi-w, spec.Hi)
|
||||
}
|
||||
brushPointer(track, window, ends, drag, value, from, set, move)
|
||||
})
|
||||
ui.Text(c, val(value[0])+" – "+val(value[1])).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Role(ui.RoleStatus)
|
||||
})
|
||||
res = core.NewChoiceResult(res.Element, *value != before)
|
||||
if res.Changed() {
|
||||
c.Announce(fmt.Sprintf(core.Msg(c, "chart_brush.window"), opts.Label, val(value[0]), val(value[1])))
|
||||
slog.Debug("mujica: chart_brush range", "from", value[0], "to", value[1])
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// brushKey handles arrow, Page, Home and End keys.
|
||||
func brushKey(e *ui.Element, v float64, spec input.NumberInputSpec) (float64, bool) {
|
||||
if e.IsDisabled() {
|
||||
return v, false
|
||||
}
|
||||
switch {
|
||||
case e.Shortcut(0, ui.KeyLeft), e.Shortcut(0, ui.KeyDown):
|
||||
return spec.StepBy(v, -1), true
|
||||
case e.Shortcut(0, ui.KeyRight), e.Shortcut(0, ui.KeyUp):
|
||||
return spec.StepBy(v, 1), true
|
||||
case e.Shortcut(0, ui.KeyPageDown):
|
||||
return spec.StepBy(v, -10), true
|
||||
case e.Shortcut(0, ui.KeyPageUp):
|
||||
return spec.StepBy(v, 10), true
|
||||
case e.Shortcut(0, ui.KeyHome):
|
||||
return spec.Lo, true
|
||||
case e.Shortcut(0, ui.KeyEnd):
|
||||
return spec.Hi, true
|
||||
}
|
||||
return v, false
|
||||
}
|
||||
|
||||
// brushPointer drags handles, the window, or a new range.
|
||||
func brushPointer(track, window *ui.Element, ends [2]*ui.Element, drag *brushDrag, value *[2]float64, from func(float32) float64, set, move func(lo, hi float64)) {
|
||||
px, _, _ := track.PointerPosition()
|
||||
at := from(px)
|
||||
parts := []*ui.Element{ends[0], ends[1], window, track}
|
||||
held := -1
|
||||
for i, e := range parts {
|
||||
if _, _, ok := e.Dragged(); ok && held < 0 {
|
||||
held = i
|
||||
}
|
||||
}
|
||||
if held < 0 {
|
||||
drag.part = -1
|
||||
return
|
||||
}
|
||||
if drag.part < 0 {
|
||||
*drag = brushDrag{part: held, grab: at - value[0], start: at}
|
||||
}
|
||||
w := value[1] - value[0]
|
||||
switch drag.part {
|
||||
case 0:
|
||||
set(min(at, value[1]), value[1])
|
||||
case 1:
|
||||
set(value[0], max(at, value[0]))
|
||||
case 2:
|
||||
lo := at - drag.grab
|
||||
// Shift both ends together at a boundary.
|
||||
move(lo, lo+w)
|
||||
default:
|
||||
set(min(drag.start, at), max(drag.start, at))
|
||||
}
|
||||
}
|
||||
|
||||
// brushPaint draws the preview and frames the selected range.
|
||||
type brushPreview struct {
|
||||
id string
|
||||
lo, hi float64
|
||||
width float32
|
||||
runs [][]int
|
||||
}
|
||||
|
||||
func brushPaint(p *ui.Painter, r ui.Rect, value [2]float64, opts ChartBrushOptions, k theme.Tokens, preview *brushPreview) {
|
||||
lo, hi := preview.lo, preview.hi
|
||||
if n := len(opts.Values); n > 1 && hi > lo {
|
||||
x := func(i int) float32 { return r.W * float32(i) / float32(n-1) }
|
||||
if preview.width != r.W || preview.runs == nil {
|
||||
preview.width, preview.runs = r.W, nil
|
||||
chartRuns(opts.Values, x, func(idx []int) { preview.runs = append(preview.runs, idx) })
|
||||
}
|
||||
for _, idx := range preview.runs {
|
||||
var line ui.Path
|
||||
for j, i := range idx {
|
||||
px := internal.Snap(p, r.X+x(i))
|
||||
py := internal.Snap(p, r.Y+4+(r.H-8)*float32(1-(opts.Values[i]-lo)/(hi-lo)))
|
||||
if j == 0 {
|
||||
line.MoveTo(px, py)
|
||||
} else {
|
||||
line.LineTo(px, py)
|
||||
}
|
||||
}
|
||||
p.StrokePath(&line, 1.25, k.TextMuted)
|
||||
}
|
||||
}
|
||||
x0 := r.X + r.W*float32((value[0]-opts.Min)/(opts.Max-opts.Min))
|
||||
x1 := r.X + r.W*float32((value[1]-opts.Min)/(opts.Max-opts.Min))
|
||||
p.Fill(ui.Rect{X: r.X, Y: r.Y, W: x0 - r.X, H: r.H}, k.Background.Alpha(0.6), 0)
|
||||
p.Fill(ui.Rect{X: x1, Y: r.Y, W: r.X + r.W - x1, H: r.H}, k.Background.Alpha(0.6), 0)
|
||||
p.Stroke(ui.Rect{X: x0, Y: r.Y, W: x1 - x0, H: r.H}, k.AccentText, 0, 1.5)
|
||||
}
|
||||
Reference in new issue
Block a user