Split into category packages; module github.com/ZacharyZhang-NY/MujicaUI

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 16:18:48 -04:00
1 parent 5a7e921c7e
commit 22b53e9e9e
1822 files changed
+24568 -21314

No files matched your search

+241
View File
@@ -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)
}