Add fonts, autotable, autotable examples

This commit is contained in:
2026-07-13 13:01:26 -04:00
parent ea3d2a6d03
commit cf8342f8d4
71 changed files with 16084 additions and 1443 deletions

198
go/webui/position_test.go Normal file
View File

@@ -0,0 +1,198 @@
package webui
import (
"testing"
"kjol/wasmruntime"
)
// A 1024x768 viewport with a 100x40 trigger in the middle, and a 200x100 panel.
var (
vp = wasmruntime.Size{Width: 1024, Height: 768}
trigger = wasmruntime.Rect{X: 400, Y: 300, Width: 100, Height: 40}
panel = wasmruntime.Rect{X: 0, Y: 0, Width: 200, Height: 100}
)
func opts(placement string) PositionOptions {
o := DefaultPositionOptions()
o.Placement = placement
return o
}
func TestPlacements(t *testing.T) {
// trigger: left=400 right=500 centerX=450, top=300 bottom=340 centerY=320
// panel: 200x100. offset 4.
cases := []struct {
placement string
top, left float64
}{
{PlacementBottomStart, 344, 400}, // below, left edges aligned
{PlacementBottom, 344, 350}, // below, centred: 450 - 100
{PlacementBottomEnd, 344, 300}, // below, right edges aligned: 500 - 200
{PlacementTopStart, 196, 400}, // above: 300 - 100 - 4
{PlacementTop, 196, 350},
{PlacementTopEnd, 196, 300},
{PlacementRightStart, 300, 504}, // right of: 500 + 4
{PlacementRight, 270, 504}, // right, centred: 320 - 50
{PlacementRightEnd, 240, 504}, // right, bottom-aligned: 340 - 100
{PlacementLeftStart, 300, 196}, // left of: 400 - 200 - 4
{PlacementLeft, 270, 196},
{PlacementLeftEnd, 240, 196},
}
for _, c := range cases {
got := ComputePosition(trigger, panel, vp, opts(c.placement))
if got.Top != c.top || got.Left != c.left {
t.Errorf("%s: got top=%v left=%v, want top=%v left=%v", c.placement, got.Top, got.Left, c.top, c.left)
}
if got.Placement != c.placement {
t.Errorf("%s: resolved placement changed to %s with room to spare", c.placement, got.Placement)
}
}
}
func TestFlipWhenItDoesNotFit(t *testing.T) {
// Trigger near the bottom: a bottom-placed panel would run off, so flip up.
low := wasmruntime.Rect{X: 400, Y: 700, Width: 100, Height: 40}
got := ComputePosition(low, panel, vp, opts(PlacementBottomStart))
if got.Placement != PlacementTopStart {
t.Fatalf("placement = %s, want top-start (should have flipped)", got.Placement)
}
if got.Top != 596 { // 700 - 100 - 4
t.Errorf("top = %v, want 596", got.Top)
}
// The alignment suffix must survive the flip.
if p := ComputePosition(low, panel, vp, opts(PlacementBottomEnd)).Placement; p != PlacementTopEnd {
t.Errorf("bottom-end flipped to %s, want top-end", p)
}
// Trigger near the right edge: right-placed flips to left.
right := wasmruntime.Rect{X: 950, Y: 300, Width: 60, Height: 40}
if p := ComputePosition(right, panel, vp, opts(PlacementRightStart)).Placement; p != PlacementLeftStart {
t.Errorf("placement = %s, want left-start", p)
}
}
// If neither side fits, keep the requested one rather than flipping into an
// equally bad spot.
func TestNoFlipWhenNeitherSideFits(t *testing.T) {
tall := wasmruntime.Rect{X: 0, Y: 0, Width: 200, Height: 700}
mid := wasmruntime.Rect{X: 400, Y: 350, Width: 100, Height: 40}
got := ComputePosition(mid, tall, vp, opts(PlacementBottomStart))
if got.Placement != PlacementBottomStart {
t.Errorf("placement = %s, want bottom-start (no better side available)", got.Placement)
}
}
// The bug we are deliberately fixing: the TSX clamped BOTH axes, so a panel that
// did not fit below got pushed up over the trigger it belonged to. Shift must
// leave the main axis alone.
func TestShiftDoesNotCoverTheTrigger(t *testing.T) {
tall := wasmruntime.Rect{X: 0, Y: 0, Width: 200, Height: 700}
low := wasmruntime.Rect{X: 400, Y: 700, Width: 100, Height: 40}
got := ComputePosition(low, tall, vp, opts(PlacementBottomStart))
if got.Top < low.Bottom() {
t.Errorf("panel top %v is above the trigger's bottom %v — it is covering its own trigger", got.Top, low.Bottom())
}
}
func TestShiftClampsTheCrossAxis(t *testing.T) {
// Trigger hard against the right edge; a bottom-start panel would overflow.
edge := wasmruntime.Rect{X: 980, Y: 300, Width: 40, Height: 40}
got := ComputePosition(edge, panel, vp, opts(PlacementBottomStart))
wantLeft := vp.Width - panel.Width - 8 // 1024 - 200 - padding
if got.Left != wantLeft {
t.Errorf("left = %v, want %v (clamped to the viewport)", got.Left, wantLeft)
}
// And against the left edge.
edge = wasmruntime.Rect{X: 2, Y: 300, Width: 40, Height: 40}
if got := ComputePosition(edge, panel, vp, opts(PlacementBottomEnd)); got.Left != 8 {
t.Errorf("left = %v, want 8 (clamped to the padding)", got.Left)
}
}
// The second flaw we fix: after a shift, the arrow must still point at the trigger.
func TestArrowTracksTheTriggerAfterShift(t *testing.T) {
o := opts(PlacementBottom)
o.ArrowSize = 8
// Unshifted, centred: the arrow sits at the panel's middle.
got := ComputePosition(trigger, panel, vp, o)
if got.ArrowOffset != 100 { // panel is 200 wide
t.Errorf("centred arrow offset = %v, want 100", got.ArrowOffset)
}
// Hard against the right edge, the panel gets shifted left but the trigger did
// not move — so the arrow must slide right to keep pointing at it.
edge := wasmruntime.Rect{X: 980, Y: 300, Width: 40, Height: 40} // centre x = 1000
got = ComputePosition(edge, panel, vp, o)
wantOffset := edge.CenterX() - got.Left // 1000 - 816 = 184
if got.ArrowOffset != wantOffset {
t.Errorf("arrow offset = %v, want %v (pointing at the trigger centre)", got.ArrowOffset, wantOffset)
}
if got.ArrowOffset <= 100 {
t.Error("arrow did not move toward the trigger after the panel was shifted")
}
}
// The arrow must never leave the panel, however far the trigger is.
func TestArrowIsClampedToThePanel(t *testing.T) {
o := opts(PlacementBottomStart)
o.ArrowSize = 8
o.ArrowPadding = 4
far := wasmruntime.Rect{X: 1000, Y: 300, Width: 20, Height: 40}
got := ComputePosition(far, panel, vp, o)
lo, hi := 8.0, panel.Width-8 // padding + half the arrow, at both ends
if got.ArrowOffset < lo || got.ArrowOffset > hi {
t.Errorf("arrow offset %v escaped the panel [%v, %v]", got.ArrowOffset, lo, hi)
}
}
// The third fix: tell the caller how much room there is, so a long menu can scroll
// instead of running off the screen.
func TestAvailableSpace(t *testing.T) {
got := ComputePosition(trigger, panel, vp, opts(PlacementBottomStart))
want := vp.Height - trigger.Bottom() - 4 - 8 // viewport - trigger bottom - offset - padding
if got.MaxHeight != want {
t.Errorf("MaxHeight = %v, want %v", got.MaxHeight, want)
}
if got.MaxWidth != 0 {
t.Errorf("MaxWidth = %v, want 0 (unconstrained on the cross axis)", got.MaxWidth)
}
got = ComputePosition(trigger, panel, vp, opts(PlacementRightStart))
if got.MaxWidth != vp.Width-trigger.Right()-12 {
t.Errorf("MaxWidth = %v, want %v", got.MaxWidth, vp.Width-trigger.Right()-12)
}
if got.MaxHeight != 0 {
t.Errorf("MaxHeight = %v, want 0", got.MaxHeight)
}
}
func TestOppositeSide(t *testing.T) {
for placement, want := range map[string]string{
PlacementTopStart: "bottom",
PlacementBottom: "top",
PlacementLeftEnd: "right",
PlacementRight: "left",
} {
if got := OppositeSide(placement); got != want {
t.Errorf("OppositeSide(%s) = %s, want %s", placement, got, want)
}
}
}
func TestUnknownPlacementFallsBackToBottom(t *testing.T) {
got := ComputePosition(trigger, panel, vp, opts("nonsense"))
if got.Placement != "bottom-center" && got.Placement != "bottom" {
t.Errorf("placement = %s, want a bottom variant", got.Placement)
}
if got.Top != 344 {
t.Errorf("top = %v, want 344 (positioned below)", got.Top)
}
}