Upgrade MyGo to v0.5.5

MyGo v0.5.5 returns ui.Element as a checked value handle instead of a
pointer. Migrated with `mygo migrate-ui`, then fixed what the codemod
leaves to review:

- Element nil checks become .Valid(); absent elements are ui.Element{}.
- Input handlers and paint callbacks run outside the build pass, where
  elements expire and Context methods are inert: read IsDisabled into a
  local during the build, keep a ui.Handle for Focus, use c.Services()
  for clipboard access, create ui.Local state during the build.
- Add core.AutoFocus: v0.5.5 records an overlay's opener after its
  content builds, so same-frame autofocus lost focus restore on close.
  It focuses one frame later; cascader rows defer their focus likewise.
- core.OverlayLayer identifies build passes by the root element, not
  c.Now(), which repeats across passes and under the Tester clock.
- Context menus survive the opening press counted as PressedOutside;
  split button tooltip no longer reorders its open menu; disabled
  menubar rows pass presses to the panel; Form defers blur-started
  async checks so the growing message no longer moves Submit away
  from the release; Combobox drops its disabled copy now that MyGo
  blocks typing under a disabled wrapper (canary flipped to a guard).
- The Tester clock is virtual: tests use Advance/Now instead of
  time.Sleep/time.Now and read element state inside the view. The web
  mirror advances the clock to wall time before each frame and input.
- AGENTS.md and docs describe the value Element API and the new pin.

Snapshot goldens are darwin-only and were not re-verified here.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-17 01:37:54 -04:00
1 parent bc716d27be
commit bcce34cda1
603 files changed
+1366 -1277

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ func TestAreaChartStackedReadout(t *testing.T) {
if got := chartReadout(tt); got != "0 · Pews 3 · Candles 5" {
t.Fatalf("readout shows the series values, not the stack: %q", got)
}
b := h.res.Element.Bounds()
b := h.box(tt)
tt.Move(b.X+52+(b.W-64)*0.5, b.Y+b.H/2)
if got := chartReadout(tt); got != "2 · Pews no value · Candles 6" || h.res.Point != 2 {
t.Fatalf("hover %q", got)
+1 -1
View File
@@ -44,7 +44,7 @@ func TestBarChartHoverStackedHorizontal(t *testing.T) {
tt := ui.NewTester(h.view, 520, 420)
tabTo(t, tt, "Tithes")
found := map[string]bool{}
b := h.res.Element.Bounds()
b := h.box(tt)
for y := b.Y + 30; y < b.Y+b.H-40; y += 4 {
for x := b.X + 60; x < b.X+b.W-16; x += 8 {
tt.Move(x, y)
+1 -1
View File
@@ -33,7 +33,7 @@ func TestBoxPlotReadout(t *testing.T) {
if got := chartReadout(tt); got != "Choir · median 5.5 · quartiles 3.25–7.75 · whiskers 1–9 · 1 outliers" {
t.Fatalf("readout %q", got)
}
b := h.res.Element.Bounds()
b := h.box(tt)
tt.Key(0, ui.KeyEscape)
tt.Move(b.X+52+(b.W-64)*0.75, b.Y+100)
if got := chartReadout(tt); got != "Nave · median 5 · quartiles 5–6 · whiskers 4–7 · 0 outliers" {
+2 -2
View File
@@ -23,7 +23,7 @@ func TestBulletChartReadout(t *testing.T) {
}
}
tt.Key(0, ui.KeyEscape)
b := h.res.Element.Bounds()
b := h.box(tt)
plotW := b.W - 24
tt.Move(b.X+8+plotW*0.1, b.Y+6+21) // band 1, inside the value bar
if got := chartReadout(tt); got != "Actual 270" {
@@ -40,7 +40,7 @@ func TestBulletChartVerticalAndBad(t *testing.T) {
return BulletChart(c, 40, BulletChartOptions{Frame: ChartFrame{Label: "Alms", Height: 200}, Vertical: true, Max: 100})
}}
tt := ui.NewTester(h.view, 520, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
tt.Move(b.X+b.W/2, b.Y+8+184*0.8) // low on the scale: inside the bar
if got := chartReadout(tt); got != "Actual 40" {
t.Fatalf("vertical hover %q", got)
+1 -1
View File
@@ -65,7 +65,7 @@ func TestCalendarHeatmapWeekStartHover(t *testing.T) {
return CalendarHeatmap(c, &year, calendarValues(), CalendarHeatmapOptions{Frame: ChartFrame{Label: "Prayers"}, WeekStart: tc.start})
}}
tt := ui.NewTester(h.view, 560, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
plotY := b.Y + b.H - 20 - 8 - 240 // above the readout line and gap
step := min((b.W-36)/53, float32(240-48)/7)
tt.Move(b.X+32+step/2, plotY+18+float32(tc.row)*step+step/2)
+1 -1
View File
@@ -19,7 +19,7 @@ type CandleCountdownOptions struct {
}
// CandleCountdown displays closing time and schedules visible updates.
func CandleCountdown(c *ui.Context, interval Interval, opts CandleCountdownOptions) *ui.Element {
func CandleCountdown(c *ui.Context, interval Interval, opts CandleCountdownOptions) ui.Element {
loc := opts.Location
if loc == nil {
loc = time.UTC
+1 -1
View File
@@ -121,7 +121,7 @@ func CandlestickChart(c *ui.Context, candles []Candle, view *MarketView, opts Ca
m.yDomain()
m.alertsCheck()
res, plot := ch.Build(m.spec())
if plot != nil {
if plot.Valid() {
m.input(plot)
}
if s := opts.Sync; s != nil {
+2 -3
View File
@@ -33,13 +33,12 @@ func mktState0(r ChartResult) *mktState {
func TestCandlestickChartDrawsOnlyVisible(t *testing.T) {
candles := sampleCandles(100000)
var view MarketView
var res ChartResult
var st *mktState
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
res = CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC"}})
st = mktState0(CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC"}}))
}, 900, 400)
tt.Frame()
st := mktState0(res)
if st.drawn != 120 {
t.Fatalf("drew %d candles of 100000", st.drawn)
}
+2 -2
View File
@@ -123,7 +123,7 @@ type noteDrag struct {
}
// Drag handles movement; plot uses element coordinates.
func (a ChartAnnotationPart) Drag(plotElement *ui.Element, plot ui.Rect) bool {
func (a ChartAnnotationPart) Drag(plotElement ui.Element, plot ui.Rect) bool {
if plotElement.IsDisabled() {
return false
}
@@ -185,7 +185,7 @@ func (a ChartAnnotationPart) Drag(plotElement *ui.Element, plot ui.Rect) bool {
return true
}
func (a ChartAnnotationPart) keyboard(e *ui.Element) bool {
func (a ChartAnnotationPart) keyboard(e ui.Element) bool {
index := ui.Local(e, "mujica.notes.key", func() int { return 0 })
dir := 0
if e.Shortcut(ui.Alt, ui.KeyPageDown) {
+2 -2
View File
@@ -19,7 +19,7 @@ func TestChartAnnotationDrag(t *testing.T) {
}
h := &chartHarness{build: func(c *ui.Context) ChartResult { return partsChart(c, notes, false) }}
tt := ui.NewTester(h.view, 520, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
plot := ChartInset(ui.Rect{X: b.X, Y: b.Y, W: b.W, H: 240}, 52, 26)
yAt := func(v float32) float32 { return plot.Y + plot.H*(1-v/100) }
xAt := func(v float32) float32 { return plot.X + plot.W*v/10 }
@@ -51,7 +51,7 @@ func TestChartAnnotationDisabledAndBad(t *testing.T) {
notes := []ChartNote{{Kind: NoteHLine, Value: 50, Draggable: true}}
h := &chartHarness{disabled: true, build: func(c *ui.Context) ChartResult { return partsChart(c, notes, false) }}
tt := ui.NewTester(h.view, 520, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
plot := ChartInset(ui.Rect{X: b.X, Y: b.Y, W: b.W, H: 240}, 52, 26)
tt.Press(plot.X+100, plot.Y+plot.H/2)
tt.Move(plot.X+100, plot.Y+20)
+1 -1
View File
@@ -96,7 +96,7 @@ func partsChart(c *ui.Context, notes []ChartNote, crosshair bool) ChartResult {
return hair.Snap(len(pts), func(i int) (float32, float32) { return at(r, i) }, px, py)
},
Anchor: at,
Input: func(plot *ui.Element, r ui.Rect) { notesPart.Drag(plot, plotOf(r)) },
Input: func(plot ui.Element, r ui.Rect) { notesPart.Drag(plot, plotOf(r)) },
Draw: func(p *ui.Painter, r ui.Rect, active int) {
plot := plotOf(r)
grid.Draw(p, plot)
+5 -5
View File
@@ -94,7 +94,7 @@ func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) core.C
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 {
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)
@@ -104,7 +104,7 @@ func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) core.C
})
}
// Tab order runs start, window, end.
ends := [2]*ui.Element{end(0, x0)}
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)
@@ -149,7 +149,7 @@ func ChartBrush(c *ui.Context, value *[2]float64, opts ChartBrushOptions) core.C
}
// brushKey handles arrow, Page, Home and End keys.
func brushKey(e *ui.Element, v float64, spec input.NumberInputSpec) (float64, bool) {
func brushKey(e ui.Element, v float64, spec input.NumberInputSpec) (float64, bool) {
if e.IsDisabled() {
return v, false
}
@@ -171,10 +171,10 @@ func brushKey(e *ui.Element, v float64, spec input.NumberInputSpec) (float64, bo
}
// 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)) {
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}
parts := []ui.Element{ends[0], ends[1], window, track}
held := -1
for i, e := range parts {
if _, _, ok := e.Dragged(); ok && held < 0 {
+8 -5
View File
@@ -14,14 +14,14 @@ type brushHarness struct {
value [2]float64
changes int
disabled bool
el *ui.Element
bounds ui.Rect
}
func (h *brushHarness) view(c *ui.Context) {
core.Use(c, core.Settings{Mode: core.Light})
ui.Column(c).Padding(10).Width(420).Children(func() {
r := ChartBrush(c, &h.value, ChartBrushOptions{Label: "Months", Min: 0, Max: 100, Step: 1, Disabled: h.disabled})
h.el = r.Element
h.bounds = r.Element.Bounds()
if r.Changed() {
h.changes++
}
@@ -57,7 +57,8 @@ func TestChartBrushKeyboard(t *testing.T) {
func TestChartBrushDrag(t *testing.T) {
h := &brushHarness{value: [2]float64{20, 40}}
tt := ui.NewTester(h.view, 460, 200)
b := h.el.Bounds()
tt.Frame() // Bounds reads the previous frame
b := h.bounds
x := func(v float32) float32 { return b.X + 400*v/100 }
y := b.Y + 24
tt.Press(x(30), y) // inside the window
@@ -86,7 +87,8 @@ func TestChartBrushDrag(t *testing.T) {
func TestChartBrushDisabledAndBad(t *testing.T) {
h := &brushHarness{value: [2]float64{20, 40}, disabled: true}
tt := ui.NewTester(h.view, 460, 200)
b := h.el.Bounds()
tt.Frame() // Bounds reads the previous frame
b := h.bounds
tt.Press(b.X+120, b.Y+24)
tt.Move(b.X+200, b.Y+24)
tt.Release(b.X+200, b.Y+24)
@@ -128,7 +130,8 @@ func TestChartBrushSnapshot(t *testing.T) {
func TestChartBrushWindowDragPreservesSpanAtEdges(t *testing.T) {
h := &brushHarness{value: [2]float64{20, 60}}
tt := ui.NewTester(h.view, 460, 200)
b := h.el.Bounds()
tt.Frame() // Bounds reads the previous frame
b := h.bounds
tt.Press(b.X+160, b.Y+24)
tt.Move(b.X+380, b.Y+24)
tt.Release(b.X+380, b.Y+24)
+1 -1
View File
@@ -14,7 +14,7 @@ import (
func TestChartCrosshairSnapsAndFollowsKeys(t *testing.T) {
h := &chartHarness{build: func(c *ui.Context) ChartResult { return partsChart(c, nil, true) }}
tt := ui.NewTester(h.view, 520, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
plot := ChartInset(ui.Rect{X: b.X, Y: b.Y, W: b.W, H: 240}, 52, 26)
// Point 1 is ten DIPs from the pointer.
px, py := plot.X+plot.W/2, plot.Y+plot.H*0.2
+8 -5
View File
@@ -13,30 +13,33 @@ func TestChartEmptyStateKeepsSizeAndRetries(t *testing.T) {
status := data.DataReady
retries, disabled := 0, false
var heights []float32
var el *ui.Element
var box ui.Rect
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Column(c).Width(400).Disabled(disabled).Children(func() {
r := ChartEmptyState(c, status, ChartEmptyStateOptions{Height: 180, Empty: "No hymns this week", Error: "The choir did not answer"})
el = r.Element
box = r.Element.Bounds()
if r.Changed() {
retries++
}
})
}, 440, 300)
heights = append(heights, el.Bounds().H)
tt.Frame() // Bounds reads the previous frame
heights = append(heights, box.H)
if !tt.HasText("No hymns this week") {
t.Fatalf("empty: %q", tt.Texts())
}
status = data.DataLoading
tt.Frame()
heights = append(heights, el.Bounds().H)
tt.Frame() // Bounds reads the previous frame
heights = append(heights, box.H)
if !tt.HasText("Loading") {
t.Fatalf("loading: %q", tt.Texts())
}
status = data.DataFailed
tt.Frame()
heights = append(heights, el.Bounds().H)
tt.Frame() // Bounds reads the previous frame
heights = append(heights, box.H)
if err := tt.Click("Retry"); err != nil || retries != 1 || !tt.HasText("The choir did not answer") {
t.Fatalf("retry: %v %d", err, retries)
}
+5 -5
View File
@@ -26,7 +26,7 @@ type ChartFrame struct {
// ChartResult is a built chart and this frame's events.
type ChartResult struct {
Element *ui.Element
Element ui.Element
// Point is the hovered or focused mark, -1 for none.
Point int
changed, retry bool
@@ -69,7 +69,7 @@ type ChartSpec struct {
// Tooltip writes mark i's tooltip; nil splits Readout at " · ".
Tooltip func(i int) []ChartTooltipLine
// Input takes the plot's other input, as a ChartAnnotation's drags.
Input func(plot *ui.Element, r ui.Rect)
Input func(plot ui.Element, r ui.Rect)
}
// Chart frames a custom chart: legend, focus, keys, readout, tooltip and empty states.
@@ -120,10 +120,10 @@ func (ch *chart) Toggles(on bool) *map[string]bool {
func (ch *chart) Hidden(name string) bool { return ch.st.hidden[name] }
// Build lays out legend, plot and readout; returns the plot.
func (ch *chart) Build(spec ChartSpec) (ChartResult, *ui.Element) {
func (ch *chart) Build(spec ChartSpec) (ChartResult, ui.Element) {
c, k, st := ch.c, core.Tokens(ch.c), ch.st
u := core.Density(c).Unit()
var plot *ui.Element
var plot ui.Element
ch.Res.Element.Children(func() {
if len(spec.Legend) > 0 {
if name, ok := ChartLegend(c, spec.Hidden, spec.Legend, ChartLegendOptions{}).Toggled(); ok {
@@ -181,7 +181,7 @@ func (ch *chart) Build(spec ChartSpec) (ChartResult, *ui.Element) {
}
// input moves the keyboard mark, and takes clicks on marks.
func (ch *chart) input(plot *ui.Element, spec ChartSpec) {
func (ch *chart) input(plot ui.Element, spec ChartSpec) {
st := ch.st
if plot.IsDisabled() {
return
+1 -1
View File
@@ -83,7 +83,7 @@ func TestChartsIgnoreInputWhenDisabled(t *testing.T) {
tt.Key(0, ui.KeyEnter)
tt.Key(0, ui.KeySpace)
}
b := h.res.Element.Bounds()
b := h.box(tt)
tt.Click("Under test")
tt.ClickAt(b.X+b.W/2, b.Y+b.H/2)
if h.res.Point != -1 || values != 0 {
+3 -3
View File
@@ -29,7 +29,7 @@ type ChartLegendOptions struct {
// ChartLegendResult exposes the element and legend events.
type ChartLegendResult struct {
Element *ui.Element
Element ui.Element
toggled string
expanded, folded bool
}
@@ -68,7 +68,7 @@ func ChartLegend(c *ui.Context, hidden *map[string]bool, items []ChartLegendItem
shown = items[:opts.MaxItems]
}
for _, it := range shown {
ui.Row(c).Key(it.Name).Children(func() {
ui.Row(c.Key(it.Name)).Children(func() {
if name, ok := chartLegendEntry(c, hidden, it, res.Element.IsDisabled()); ok {
res.toggled = name
}
@@ -100,7 +100,7 @@ func chartLegendEntry(c *ui.Context, hidden *map[string]bool, it ChartLegendItem
k := core.Tokens(c)
u := core.Density(c).Unit()
on := hidden == nil || !(*hidden)[it.Name]
var e *ui.Element
var e ui.Element
switched := false
if hidden != nil {
e = ui.CheckboxBase(c, &on).Disabled(disabled).Label(it.Name)
+2 -2
View File
@@ -18,7 +18,7 @@ type ChartTooltipLine struct {
}
// ChartTooltip shows readouts beside a window point, avoiding edges.
func ChartTooltip(c *ui.Context, x, y float32, lines []ChartTooltipLine) *ui.Element {
func ChartTooltip(c *ui.Context, x, y float32, lines []ChartTooltipLine) ui.Element {
if len(lines) == 0 {
panic("mujica: ChartTooltip needs a line")
}
@@ -45,7 +45,7 @@ func ChartTooltip(c *ui.Context, x, y float32, lines []ChartTooltipLine) *ui.Ele
} else {
left = max(4, min(x-gap-w, ww-4-w))
}
var tip *ui.Element
var tip ui.Element
ui.Overlay(c, func() {
tip = ui.Column(c).Absolute().Left(left).Top(top).Size(w, h).Role(ui.RoleNone).PassThrough()
core.OverlayStyle(c, tip, core.OverlayEnter(c, tip))
+1 -1
View File
@@ -53,7 +53,7 @@ func TestChartTooltipFollowsFocusAndPointer(t *testing.T) {
if tooltipShown(tt) {
t.Fatal("tooltip stayed after Escape")
}
b := h.res.Element.Bounds()
b := h.box(tt)
tt.Move(b.X+b.W/2, b.Y+b.H/2)
if !tooltipShown(tt) {
t.Fatal("no tooltip under the pointer")
+3 -2
View File
@@ -42,7 +42,7 @@ func TestChordDiagramReadout(t *testing.T) {
t.Fatalf("chord %q", got)
}
tt.Key(0, ui.KeyEscape)
b := h.res.Element.Bounds()
b := h.box(tt)
cx, cy, rad := b.X+b.W/2, b.Y+120, float32(240)/2-30
tt.Move(cx+(rad+6)*0.5, cy-(rad+6)*0.866) // 30°: inside the first arc
if h.res.Point != 0 {
@@ -95,8 +95,9 @@ func TestChordDiagramDenseDataBoundedAndDisabled(t *testing.T) {
h := &chartHarness{build: func(c *ui.Context) ChartResult {
return ChordDiagram(c, names, matrix, ChordDiagramOptions{Frame: ChartFrame{Label: "Dense chords"}})
}}
var shown *[]int
h.built = func(r ChartResult) { shown = ui.Local(r.Element, "mujica.chord.visible", func() []int { return nil }) }
tt := ui.NewTester(h.view, 520, 420)
shown := ui.Local(h.res.Element, "mujica.chord.visible", func() []int { return nil })
if len(*shown) == 0 || len(*shown) > 360 {
t.Fatalf("built %d chords", len(*shown))
}
+1 -1
View File
@@ -53,7 +53,7 @@ func DrawingToolbar(c *ui.Context, d *DrawingTools, opts DrawingToolbarOptions)
for t := ToolSelect; t <= ToolText; t++ {
name := fmt.Sprintf("%s (Alt+%d)", core.Msg(c, fmt.Sprintf("drawing_toolbar.tool%d", t)), t+1)
on := d.Tool == t
b := ui.ButtonBase(c).Key(t).Size(theme.MinHitSize+4, theme.MinHitSize+4).Radius(theme.ControlRadius).Label(name).Tooltip(name).Disabled(d.Locked)
b := ui.ButtonBase(c.Key(t)).Size(theme.MinHitSize+4, theme.MinHitSize+4).Radius(theme.ControlRadius).Label(name).Tooltip(name).Disabled(d.Locked)
internal.Ring(b, k.Focus, theme.ControlRadius)
fg := k.Text
switch {
+1 -1
View File
@@ -126,7 +126,7 @@ func (d *DrawingTools) xy(m *mktCore, g mktGeom, pt DrawingPoint) (float32, floa
}
// input places, selects, moves, and deletes chart annotations.
func (d *DrawingTools) input(m *mktCore, plot *ui.Element, g mktGeom, x, y float32, over, held bool, dx, dy float32) bool {
func (d *DrawingTools) input(m *mktCore, plot ui.Element, g mktGeom, x, y float32, over, held bool, dx, dy float32) bool {
if d.Tool < ToolSelect || d.Tool > ToolText {
panic(fmt.Sprintf("mujica: unknown DrawingTool %d", d.Tool))
}
+1 -1
View File
@@ -22,7 +22,7 @@ func TestFunnelChartReadout(t *testing.T) {
t.Fatalf("readout %q", got)
}
tt.Key(0, ui.KeyEscape)
b := h.res.Element.Bounds()
b := h.box(tt)
tt.Move(b.X+b.W/3, b.Y+4+232*0.6) // third stage band
if got := chartReadout(tt); got != "Vows · 100 · 25.0% of the stage before · 10.0% of the first" {
t.Fatalf("hover %q", got)
+1 -1
View File
@@ -28,7 +28,7 @@ func TestGaugeReadoutNamesSegment(t *testing.T) {
tt.Key(0, ui.KeyEscape)
for _, full = range []bool{false, true} {
tt.Frame()
b := h.res.Element.Bounds()
b := h.box(tt)
cx := b.X + b.W/2
var cy, rad float32
if full {
+1 -1
View File
@@ -41,7 +41,7 @@ func TestHeatmapChartHover(t *testing.T) {
return HeatmapChart(c, heatRows, heatCols, heatValues(), HeatmapChartOptions{Frame: ChartFrame{Label: "Hours"}})
}}
tt := ui.NewTester(h.view, 520, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
plotW := b.W - 72 - 64
tt.Move(b.X+72+plotW*0.6, b.Y+4+212*0.2) // third column, first row
if got := chartReadout(tt); got != "Mon · Vespers · 9" {
+2 -2
View File
@@ -44,7 +44,7 @@ type IndicatorPaneOptions struct {
// IndicatorPaneResult reports the stack's events.
type IndicatorPaneResult struct {
Element *ui.Element
Element ui.Element
// Removed reports the closed pane; -1 means none.
Removed int
// Resized reports finished height changes, for saving.
@@ -218,7 +218,7 @@ func indicatorPaneOne(c *ui.Context, candles []Candle, view *MarketView, spec In
m.view = &copy
}
result, plot := ch.Build(spec2)
if plot != nil {
if plot.Valid() {
m.input(plot)
}
return closed && !ch.Res.Element.IsDisabled(), result
+2 -2
View File
@@ -50,7 +50,7 @@ func IndicatorSelector(c *ui.Context, overlays *[]IndicatorOverlay, panes *Indic
continue
}
found++
ui.Row(c).Key(name).AlignItems(ui.Center).Gap(u * 2).MinHeight(core.Density(c).ControlHeight()).Children(func() {
ui.Row(c.Key(name)).AlignItems(ui.Center).Gap(u * 2).MinHeight(core.Density(c).ControlHeight()).Children(func() {
ui.Text(c, name).FontWeight(600).Width(56)
where := "indicator_selector.pane"
if indicatorInfo[kind].overlay {
@@ -103,7 +103,7 @@ func IndicatorSelector(c *ui.Context, overlays *[]IndicatorOverlay, panes *Indic
// indicatorSelectorActive edits parameters and reports removal of an indicator.
func indicatorSelectorActive(c *ui.Context, u float32, key string, kind IndicatorKind, params *[]float64, changed *bool) bool {
removed := false
ui.Row(c).Key(key).AlignItems(ui.Center).Gap(u * 2).Wrap().Children(func() {
ui.Row(c.Key(key)).AlignItems(ui.Center).Gap(u * 2).Wrap().Children(func() {
ui.Text(c, kind.String()).FontWeight(600).Width(56)
if len(kind.DefaultParams()) > 0 && IndicatorParamsField(c, kind, params).Changed() {
*changed = true
+1 -1
View File
@@ -87,7 +87,7 @@ func IntervalSelector(c *ui.Context, value *Interval, favorites *[]Interval, opt
// intervalSelectorRow shows a selectable period and its favorite toggle.
func intervalSelectorRow(c *ui.Context, k theme.Tokens, u float32, iv Interval, on, fav bool, favorites *[]Interval) bool {
chosen := false
ui.Row(c).Key(string(iv)).AlignItems(ui.Center).Gap(u).Children(func() {
ui.Row(c.Key(string(iv))).AlignItems(ui.Center).Gap(u).Children(func() {
b := ui.ButtonBase(c).Grow(1).MinHeight(core.Density(c).ControlHeight()).Padding(0, u*2).Justify(ui.Start).Radius(theme.ControlRadius).Gap(u * 2)
internal.Ring(b, k.Focus, theme.ControlRadius)
if on {
+15 -3
View File
@@ -14,15 +14,27 @@ import (
type chartHarness struct {
res ChartResult
bounds ui.Rect
changes int
disabled bool
build func(c *ui.Context) ChartResult
built func(r ChartResult) // reads the live element during the build
}
// box is the plot's committed bounds: Bounds reads the previous frame, so one more frame settles it.
func (h *chartHarness) box(tt *ui.Tester) ui.Rect {
tt.Frame()
return h.bounds
}
func (h *chartHarness) view(c *ui.Context) {
core.Use(c, core.Settings{Mode: core.Light})
ui.Column(c).Padding(16).Width(480).Disabled(h.disabled).Children(func() {
h.res = h.build(c)
h.bounds = h.res.Element.Bounds()
if h.built != nil {
h.built(h.res)
}
if h.res.Changed() {
h.changes++
}
@@ -84,7 +96,7 @@ func TestLineChartHoverAndToggle(t *testing.T) {
return r
}}
tt := ui.NewTester(h.view, 520, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
// The plot spans 52..W-12; x 30 is three quarters across.
px := b.X + 52 + (b.W-64)*0.75
tt.Move(px, b.Y+b.H/2)
@@ -118,11 +130,11 @@ func TestLineChartTimeAxisAndStates(t *testing.T) {
if got := chartReadout(tt); !strings.HasPrefix(got, "2026-10-05 10:00:00 · ") {
t.Fatalf("time readout %q", got)
}
heights = append(heights, h.res.Element.Bounds().H)
heights = append(heights, h.box(tt).H)
for _, st := range []data.DataStatus{data.DataLoading, data.DataFailed} {
status = st
tt.Frame()
heights = append(heights, h.res.Element.Bounds().H)
heights = append(heights, h.box(tt).H)
}
if !tt.HasText("Retry") || heights[0] != heights[1] || heights[1] != heights[2] {
t.Fatalf("states changed size: %v", heights)
+1 -1
View File
@@ -127,7 +127,7 @@ func MarketHeatmap(c *ui.Context, path *[]string, root HeatNode, opts MarketHeat
}
}
r, plot := ch.Build(spec)
if plot != nil && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 {
if plot.Valid() && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 {
*path, res.value = (*path)[:len(*path)-1], true
}
res.Element, res.Point, res.changed, res.retry = r.Element, r.Point, r.changed, r.retry
+3 -3
View File
@@ -9,9 +9,9 @@ import (
func TestMarketHeatmapAreaDrillAndKeys(t *testing.T) {
root := mktHeat()
var path []string
var got ChartResult
built := false
tt := chatTester(640, 400, func(c *ui.Context) {
got = MarketHeatmap(c, &path, root, MarketHeatmapOptions{Frame: ChartFrame{Label: "Market"}})
built = MarketHeatmap(c, &path, root, MarketHeatmapOptions{Frame: ChartFrame{Label: "Market"}}).Element.Valid()
})
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyHome)
@@ -26,7 +26,7 @@ func TestMarketHeatmapAreaDrillAndKeys(t *testing.T) {
if len(path) != 0 {
t.Fatalf("back %v", path)
}
if got.Element == nil {
if !built {
t.Fatal("element missing")
}
if root.total() != 9830 {
+3 -3
View File
@@ -12,7 +12,7 @@ import (
)
// input handles pan, zoom, alerts, drawings, and keyboard following.
func (m *mktCore) input(plot *ui.Element) {
func (m *mktCore) input(plot ui.Element) {
if plot.IsDisabled() {
return
}
@@ -20,7 +20,7 @@ func (m *mktCore) input(plot *ui.Element) {
room := m.room()
changed := false
plot.HandleInput(func(ev ui.InputEvent) bool {
if ev.Kind != ui.InputScroll || plot.IsDisabled() || ev.DY == 0 {
if ev.Kind != ui.InputScroll || ev.DY == 0 {
return false
}
f := 0.9
@@ -90,7 +90,7 @@ func (m *mktCore) input(plot *ui.Element) {
}
}
func (m *mktCore) alertsKeyboard(plot *ui.Element) {
func (m *mktCore) alertsKeyboard(plot ui.Element) {
if m.opts.Alerts == nil || len(*m.opts.Alerts) == 0 || m.base != 0 {
return
}
+6 -2
View File
@@ -14,12 +14,14 @@ func TestDepthChartLargeDataBounded(t *testing.T) {
bids[i] = DepthLevel{100 - float64(i)*0.0001, 1}
asks[i] = DepthLevel{101 + float64(i)*0.0001, 2}
}
var drawnAt *int
var res ChartResult
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
res = DepthChart(c, bids, asks, DepthChartOptions{Frame: ChartFrame{Label: "Depth"}})
drawnAt = ui.Local(res.Element, "mujica.depth.drawn", func() int { return 0 })
}, 640, 340)
drawn := *ui.Local(res.Element, "mujica.depth.drawn", func() int { return 0 })
drawn := *drawnAt
if drawn <= 0 || drawn > 4*640+8 {
t.Fatalf("painted %d depth vertices", drawn)
}
@@ -35,12 +37,14 @@ func TestVolumeProfileLargeDataBounded(t *testing.T) {
for i := range bins {
bins[i] = ProfileBin{float64(i), float64(i + 1), float64(i%19 + 1)}
}
var drawnAt *int
var res ChartResult
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
res = VolumeProfile(c, bins, VolumeProfileOptions{Frame: ChartFrame{Label: "Profile"}})
drawnAt = ui.Local(res.Element, "mujica.profile.drawn", func() int { return 0 })
}, 640, 340)
drawn := *ui.Local(res.Element, "mujica.profile.drawn", func() int { return 0 })
drawn := *drawnAt
if drawn <= 0 || drawn > 4*340+4 {
t.Fatalf("painted %d profile bins", drawn)
}
+1 -1
View File
@@ -89,7 +89,7 @@ func CompareLegend(c *ui.Context, symbols *[]CompareSymbol) core.ChoiceResult {
row := ui.Row(c).Wrap().Gap(u * 2).Role(ui.RoleList).Label(core.Msg(c, "compare_symbol.label"))
row.Children(func() {
for i, s := range *symbols {
ui.Row(c).Key(s.Name).AlignItems(ui.Center).Gap(u).Padding(0, 0, 0, u*2).Radius(6).Border(1, k.Border).Role(ui.RoleListItem).Label(s.Name).Children(func() {
ui.Row(c.Key(s.Name)).AlignItems(ui.Center).Gap(u).Padding(0, 0, 0, u*2).Radius(6).Border(1, k.Border).Role(ui.RoleListItem).Label(s.Name).Children(func() {
col := pal[(i+1)%len(pal)]
ui.Box(c).Size(10, 10).Draw(func(p *ui.Painter, r ui.Rect) { internal.Marker(p, i+1, r.X+5, r.Y+5, 9, col) })
ui.Text(c, s.Name).SingleLine()
+3 -4
View File
@@ -133,15 +133,14 @@ func TestMarketMissingComparisonCache(t *testing.T) {
for i := range s[0].Closes {
s[0].Closes[i] = math.NaN()
}
var r ChartResult
var memo *ChartMemoEntry
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
r = CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "A"}, Compare: &s})
r := CandlestickChart(c, cs, &v, CandlestickOptions{Frame: ChartFrame{Label: "A"}, Compare: &s})
memo = ui.Local(r.Element, "mujica.memo.compare.missing", func() ChartMemoEntry { return ChartMemoEntry{} })
}, 640, 320)
memo := ui.Local(r.Element, "mujica.memo.compare.missing", func() ChartMemoEntry { return ChartMemoEntry{} })
before := ChartID(memo.Val.([]float64))
tt.Frame()
memo = ui.Local(r.Element, "mujica.memo.compare.missing", func() ChartMemoEntry { return ChartMemoEntry{} })
after := ChartID(memo.Val.([]float64))
t.Logf("before=%s after=%s", before, after)
if before != after {
+7 -3
View File
@@ -25,6 +25,7 @@ func TestMarketChartsStatesDisabledAndBoundaries(t *testing.T) {
status := data.DataReady
disabled, empty := false, false
var r ChartResult
var box ui.Rect
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{Density: theme.Compact})
ui.Column(c).Disabled(disabled).Width(600).Children(func() {
@@ -75,6 +76,7 @@ func TestMarketChartsStatesDisabledAndBoundaries(t *testing.T) {
}
r = CandlestickChart(c, candles, &v, opts)
}
box = r.Element.Bounds()
})
}, 620, 350)
for range 10 {
@@ -105,13 +107,15 @@ func TestMarketChartsStatesDisabledAndBoundaries(t *testing.T) {
}
disabled = false
tt.Frame()
height := r.Element.Bounds().H
tt.Frame() // Bounds reads the previous frame
height := box.H
for _, st := range []data.DataStatus{data.DataLoading, data.DataFailed, data.DataReady} {
status = st
empty = st == data.DataReady
tt.Frame()
if r.Point != -1 || r.Element.Bounds().H != height {
t.Fatalf("state %v: point %d height %v want %v", st, r.Point, r.Element.Bounds().H, height)
tt.Frame()
if r.Point != -1 || box.H != height {
t.Fatalf("state %v: point %d height %v want %v", st, r.Point, box.H, height)
}
}
})
+1 -1
View File
@@ -82,7 +82,7 @@ func MultiChartLayout(c *ui.Context, state *MultiChartState, opts MultiChartLayo
grid := ui.Box(c).Grid().Columns(cols).Gap(u * 2).Height(h).FillWidth()
grid.Children(func() {
for i := range state.Count {
box := ui.Column(c).Key(i).MinWidth(0).MinHeight(0).Height((h - u*2*float32(rows-1)) / float32(rows)).Padding(u * 2).
box := ui.Column(c.Key(i)).MinWidth(0).MinHeight(0).Height((h - u*2*float32(rows-1)) / float32(rows)).Padding(u * 2).
Background(k.Surface).Radius(theme.CardRadius).Clip().Role(ui.RoleGroup).
Label(fmt.Sprintf(core.Msg(c, "multi_chart_layout.cell"), i+1, state.Cells[i].Symbol))
if i == state.Active && state.Count > 1 {
+4 -3
View File
@@ -150,7 +150,7 @@ func NetworkGraph(c *ui.Context, s *NetworkGraphState, nodes []GraphNode, edges
})
}
res, plot := ch.Build(spec)
if plot == nil || plot.IsDisabled() {
if !plot.Valid() || plot.IsDisabled() {
return res
}
hit := func(x, y float32) string {
@@ -165,7 +165,7 @@ func NetworkGraph(c *ui.Context, s *NetworkGraphState, nodes []GraphNode, edges
}
// networkInput drags, pans, zooms and moves the focused node.
func networkInput(plot *ui.Element, s *NetworkGraphState, focus int, nodes []GraphNode, unit func(ui.Rect) float64, hit func(x, y float32) string) {
func networkInput(plot ui.Element, s *NetworkGraphState, focus int, nodes []GraphNode, unit func(ui.Rect) float64, hit func(x, y float32) string) {
b := plot.Bounds()
r := ui.Rect{W: b.W, H: b.H}
move := func(id string, dx, dy float32) {
@@ -187,8 +187,9 @@ func networkInput(plot *ui.Element, s *NetworkGraphState, focus int, nodes []Gra
s.dragging = false
slog.Debug("mujica: network_graph drag", "node", s.drag)
}
disabled := plot.IsDisabled()
plot.HandleInput(func(ev ui.InputEvent) bool {
if ev.Kind != ui.InputScroll || ev.DY == 0 || plot.IsDisabled() {
if ev.Kind != ui.InputScroll || ev.DY == 0 || disabled {
return false
}
if ev.DY < 0 {
+1 -1
View File
@@ -44,7 +44,7 @@ func TestNetworkGraphDragNodeAndPan(t *testing.T) {
return NetworkGraph(c, &s, graphNodes, graphEdges, NetworkGraphOptions{Frame: ChartFrame{Label: "House"}})
}}
tt := ui.NewTester(h.view, 520, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
// The 10×10 layout fits 240-48 DIPs, centered in the plot.
cx, cy := b.X+b.W/2, b.Y+120
ax, ay := cx-96, cy-96
+4 -2
View File
@@ -14,6 +14,7 @@ func TestOHLCChartSharesDataAndDiffersInLook(t *testing.T) {
view := MarketView{Count: 40}
ohlc := true
var res ChartResult
var st *mktState
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
if ohlc {
@@ -21,11 +22,12 @@ func TestOHLCChartSharesDataAndDiffersInLook(t *testing.T) {
} else {
res = CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "Bars"}})
}
st = mktState0(res)
}, 600, 320)
tt.Key(0, ui.KeyTab)
tt.Key(0, ui.KeyEnd)
if res.Point != 59 || mktState0(res).drawn != 40 {
t.Fatalf("point %d drawn %d", res.Point, mktState0(res).drawn)
if res.Point != 59 || st.drawn != 40 {
t.Fatalf("point %d drawn %d", res.Point, st.drawn)
}
a := encode(t, tt.Image())
ohlc = false
+1 -1
View File
@@ -137,7 +137,7 @@ func ParallelCoordinates(c *ui.Context, filters *map[string][2]float64, axes []s
}
drag := ui.Local(ch.Res.Element, "mujica.pc.drag", func() pcDrag { return pcDrag{axis: -1} })
keyAxis := ui.Local(ch.Res.Element, "mujica.pc.axis", func() int { return 0 })
spec.Input = func(plot *ui.Element, r ui.Rect) {
spec.Input = func(plot ui.Element, r ui.Rect) {
if plot.IsDisabled() {
*drag = pcDrag{axis: -1}
return
+4 -3
View File
@@ -29,7 +29,7 @@ func TestParallelCoordinatesReadoutAndBrush(t *testing.T) {
if got := chartReadout(tt); got != "Bede · Age 65 · Hymns 1 · Vigils no value" {
t.Fatalf("readout %q", got)
}
b := h.res.Element.Bounds()
b := h.box(tt)
plot := ui.Rect{X: b.X + 36, Y: b.Y + 24, W: b.W - 72, H: 240 - 32}
// Age runs 20..80 on the first axis: brush 30..55.
y := func(v float32) float32 { return plot.Y + plot.H*(1-(v-20)/60) }
@@ -58,7 +58,7 @@ func TestParallelCoordinatesHoverAndBad(t *testing.T) {
return ParallelCoordinates(c, &filters, pcAxes, pcRows, ParallelCoordinatesOptions{Frame: ChartFrame{Label: "Monks"}})
}}
tt := ui.NewTester(h.view, 520, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
plot := ui.Rect{X: b.X + 36, Y: b.Y + 24, W: b.W - 72, H: 240 - 32}
// Row 2 crosses this point between the first axes.
tt.Move(plot.X+plot.W/8, plot.Y+plot.H*(1-0.53125))
@@ -121,8 +121,9 @@ func TestParallelCoordinatesLargeDataBounded(t *testing.T) {
h := &chartHarness{build: func(c *ui.Context) ChartResult {
return ParallelCoordinates(c, &filters, pcAxes, rows, ParallelCoordinatesOptions{Frame: ChartFrame{Label: "Large parallel"}})
}}
var shown *[]int
h.built = func(r ChartResult) { shown = ui.Local(r.Element, "mujica.pc.visible", func() []int { return nil }) }
tt := ui.NewTester(h.view, 520, 420)
shown := ui.Local(h.res.Element, "mujica.pc.visible", func() []int { return nil })
if len(*shown) == 0 || len(*shown) > 480 {
t.Fatalf("built %d rows", len(*shown))
}
+1 -1
View File
@@ -34,7 +34,7 @@ func TestPieChartReadoutAndHover(t *testing.T) {
if got := chartReadout(tt); got != "Other · 8 · 8.0%" {
t.Fatalf("readout %q", got)
}
b := h.res.Element.Bounds()
b := h.box(tt)
plotTop := b.Y + 38 // below the one-line legend
cy := plotTop + 120
tt.Move(b.X+b.W/2+60, cy-40) // upper right: the first slice
+3 -5
View File
@@ -13,18 +13,16 @@ func TestPriceAlertLineDragAndTrigger(t *testing.T) {
view := MarketView{Count: 60}
alerts := []PriceAlertLine{{ID: "a", Price: 100, Label: "Buy"}}
var fired []string
var res ChartResult
_ = res
var st *mktState
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
res = CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC"}, Alerts: &alerts,
OnAlert: func(a PriceAlertLine, last Candle) { fired = append(fired, a.ID) }})
st = mktState0(CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "BTC"}, Alerts: &alerts,
OnAlert: func(a PriceAlertLine, last Candle) { fired = append(fired, a.ID) }}))
}, 800, 400)
if !tt.HasText("BTC") {
t.Fatal("not built")
}
// Drag the line from its own y, as painted.
st := mktState0(res)
plot := st.plot
y := st.ys.Map(100)
tt.Press(plot.X+100, y)
+1 -1
View File
@@ -28,7 +28,7 @@ func TestRadarChartReadout(t *testing.T) {
if got := chartReadout(tt); got != "Latin · Agnès 6 · Clare 9" {
t.Fatalf("readout %q", got)
}
b := h.res.Element.Bounds()
b := h.box(tt)
cx, cy := b.X+b.W/2, b.Y+38+120+4
tt.Key(0, ui.KeyEscape)
tt.Move(cx, cy-60) // straight up: the first axis
+1 -1
View File
@@ -47,7 +47,7 @@ func TestSankeyChartReadout(t *testing.T) {
t.Fatalf("link %q", got)
}
tt.Key(0, ui.KeyEscape)
b := h.res.Element.Bounds()
b := h.box(tt)
tt.Move(b.X+8+4, b.Y+10) // the first node, at the top left
if got := chartReadout(tt); got != "Tithes · in 0 · out 60" {
t.Fatalf("hover %q", got)
+1 -1
View File
@@ -130,7 +130,7 @@ func ScatterChart(c *ui.Context, selected *[]ScatterKey, groups []ScatterGroup,
}
}
res, plot := ch.Build(spec)
if plot != nil && !plot.IsDisabled() {
if plot.Valid() && !plot.IsDisabled() {
x, y, _ := plot.PointerPosition()
if _, _, held := plot.Dragged(); held {
if !drag.on {
+1 -1
View File
@@ -47,7 +47,7 @@ func TestScatterChartBoxSelect(t *testing.T) {
return ScatterChart(c, &sel, scatterGroups, ScatterChartOptions{Frame: ChartFrame{Label: "Choir"}})
}}
tt := ui.NewTester(h.view, 520, 420)
b := h.res.Element.Bounds()
b := h.box(tt)
top := b.Y + 38
// Nice domains: x 0..10 over 52..W-12; y 0..10 over top+8..top+214.
x := func(v float32) float32 { return b.X + 52 + (b.W-64)*v/10 }
+4 -4
View File
@@ -150,14 +150,14 @@ func TestSparkBarFiniteWidthAndStates(t *testing.T) {
func TestSparkBarMinimumHitWidth(t *testing.T) {
for _, values := range [][]float64{nil, {1}} {
var result ChartResult
var box ui.Rect
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
result = SparkBar(c, values, SparkBarOptions{Frame: ChartFrame{Label: "Bars"}})
box = SparkBar(c, values, SparkBarOptions{Frame: ChartFrame{Label: "Bars"}}).Element.Bounds()
}, 200, 100)
tt.Frame()
if result.Element.Bounds().W < 28 {
t.Fatalf("hit width %v", result.Element.Bounds().W)
if box.W < 28 {
t.Fatalf("hit width %v", box.W)
}
}
}
+1 -1
View File
@@ -146,7 +146,7 @@ func Sunburst(c *ui.Context, path *[]string, root ChartNode, opts SunburstOption
ChartText(p, ChartValue(c, sum, opts.Format), cx, cy+2, core.FontSize(c, theme.BodySize), k.Text, 0.5, 0)
}
r, plot := ch.Build(spec)
if plot != nil && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 {
if plot.Valid() && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 {
*path, res.value = (*path)[:len(*path)-1], true
}
res.Point, res.changed, res.retry = r.Point, r.changed, r.retry
+1 -1
View File
@@ -29,7 +29,7 @@ func TestSunburstReadoutAndDrill(t *testing.T) {
if err := tt.Click("Abbey"); err != nil || len(path) != 0 {
t.Fatalf("breadcrumb back: %v %v", err, path)
}
b := h.res.Element.Bounds()
b := h.box(tt)
plotTop := b.Y + b.H - 20 - 8 - 240
cx, cy := b.X+b.W/2, plotTop+120
tt.Key(0, ui.KeyEscape)
+1 -1
View File
@@ -89,7 +89,7 @@ func Treemap(c *ui.Context, path *[]string, root ChartNode, opts TreemapOptions)
}
}
r, plot := ch.Build(spec)
if plot != nil && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 {
if plot.Valid() && !plot.IsDisabled() && plot.Shortcut(0, ui.KeyBackspace) && len(*path) > 0 {
*path, res.value = (*path)[:len(*path)-1], true
}
res.Element, res.Point, res.changed, res.retry = r.Element, r.Point, r.changed, r.retry
+1 -1
View File
@@ -26,7 +26,7 @@ type TrendIndicatorOptions struct {
}
// TrendIndicator shows signed change, direction, and semantic color.
func TrendIndicator(c *ui.Context, change float64, opts TrendIndicatorOptions) *ui.Element {
func TrendIndicator(c *ui.Context, change float64, opts TrendIndicatorOptions) ui.Element {
if opts.Precision < 0 || math.IsNaN(change) || math.IsInf(change, 0) {
panic("mujica: TrendIndicator needs a number and Precision ≥ 0")
}
+3 -2
View File
@@ -20,7 +20,7 @@ func TestViolinPlotReadout(t *testing.T) {
t.Fatalf("readout %q", got)
}
tt.Key(0, ui.KeyEscape)
b := h.res.Element.Bounds()
b := h.box(tt)
tt.Move(b.X+52+(b.W-64)/6, b.Y+120)
if h.res.Point != 0 {
t.Fatalf("hover %d", h.res.Point)
@@ -62,13 +62,14 @@ func TestViolinPlotLargeDataAndDisabled(t *testing.T) {
h := &chartHarness{build: func(c *ui.Context) ChartResult {
return ViolinPlot(c, groups, ViolinPlotOptions{Frame: ChartFrame{Label: "Large violin"}})
}}
var shown *[]int
h.built = func(r ChartResult) { shown = ui.Local(r.Element, "mujica.violin.visible", func() []int { return nil }) }
tt := ui.NewTester(h.view, 520, 420)
tabTo(t, tt, "Large violin")
tt.Key(0, ui.KeyRight)
if chartReadout(tt) != "Sample · median 49.5 · quartiles 24.75–74.25 · 100000 values" {
t.Fatalf("large readout %q", chartReadout(tt))
}
shown := ui.Local(h.res.Element, "mujica.violin.visible", func() []int { return nil })
if len(*shown) != 1 {
t.Fatalf("built %d violins", len(*shown))
}
+3 -2
View File
@@ -74,6 +74,7 @@ func VolumeChart(c *ui.Context, candles []Candle, view *MarketView, opts VolumeC
return strings.Join([]string{cd.Time.Format("2006-01-02 15:04"), core.Msg(c, "candlestick_chart.volume") + " " + data.StatisticFormat(cd.Volume, 0),
core.TrendWord(c, core.TrendSign(cd.Close-cd.Open))}, " · ")
}
st := m.st0() // painting runs after the build, when Local is gone
spec.Draw = func(p *ui.Painter, r ui.Rect, active int) {
plot, band, ys, ticks, step := geom(r)
internal.AxisY(p, plot, ys, ticks, func(v float64) string { return volumeShort(v, step) }, size, k)
@@ -99,14 +100,14 @@ func VolumeChart(c *ui.Context, candles []Candle, view *MarketView, opts VolumeC
p.Fill(bar, col, 0)
}
}
m.st0().drawn = m.to - m.from
st.drawn = m.to - m.from
if at >= m.from && at < m.to {
x := internal.Snap(p, band.Center(at-m.from))
p.Line(x, plot.Y, x, plot.Y+plot.H, 1, k.TextMuted)
}
}
res, plot := ch.Build(spec)
if plot != nil && !plot.IsDisabled() {
if plot.Valid() && !plot.IsDisabled() {
room := m.room()
switch {
case plot.Shortcut(ui.Shift, ui.KeyLeft):
+3 -1
View File
@@ -13,15 +13,17 @@ func TestVolumeChartSharesViewAndReads(t *testing.T) {
candles := sampleCandles(100000)
view := MarketView{Count: 60}
var price, vol ChartResult
var volSt *mktState
tt := ui.NewTester(func(c *ui.Context) {
core.Use(c, core.Settings{})
ui.Column(c).Width(800).Children(func() {
price = CandlestickChart(c, candles, &view, CandlestickOptions{Frame: ChartFrame{Label: "Price"}})
vol = VolumeChart(c, candles, &view, VolumeChartOptions{Frame: ChartFrame{Label: "Volume"}})
volSt = mktState0(vol)
})
}, 820, 600)
tt.Frame()
if d := mktState0(vol).drawn; d != 60 {
if d := volSt.drawn; d != 60 {
t.Fatalf("volume drew %d bars", d)
}
tt.Key(0, ui.KeyTab)