Merge branch 'worktree-agent-a085d9a07ef787c18'

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 13:15:52 -04:00
commit 0acf0c30eb
4 files changed
+68 -27

No files matched your search

+1 -1
View File
@@ -39,7 +39,7 @@ if mujica.NumberInput(c, &copies, mujica.NumberInputOptions{Min: 1, Max: 99, Lab
## 键盘操作
步进沿以 `Min`(无界时为 0)为起点、`Step` 为间隔的网格移动,结果按精度取整,并停在范围内(范围先向内取到精度上,如 `Max` 0.29999999、步长 0.1 时最大值为 0.2)。不在网格上的值先吸附到相邻网格点。
步进沿以 `Min`(无界时为 0)为起点、`Step` 为间隔的网格移动,结果按精度取整,并停在范围内(范围先向内取到精度上,如 `Max` 0.29999999、步长 0.1 时最大值为 0.2)。不在网格上的值先走到该方向上最近的网格点。网格序号按整数计算,结果与原始 `Min`/`Max` 精确比较,越界则退回一格,不使用容差。
输入模式:Up/Down 按步长增减。仅步进模式:Up/Right 增加、Down/Left 减少、Home 到 `Min`、End 到 `Max`(如 [0, 10] 步长 3 时 End 为 10,再 Down 为 9)。
+26
View File
@@ -175,3 +175,29 @@ func TestRadioGroupKeysByEquality(t *testing.T) {
t.Fatalf("after reorder chose %v, One focused %v", v, tt.Focused("One"))
}
}
func TestNumberInputStepExactBounds(t *testing.T) {
for _, stepper := range []bool{false, true} {
v := 1e12
tt := numberInputStepTester(&v, NumberInputOptions{Min: 0, Max: 2e12, Step: 1, StepperOnly: stepper})
tt.Key(0, ui.KeyUp)
if v != 1e12+1 {
t.Errorf("stepper %v: Up from 1e12 by 1 gave %.0f", stepper, v)
}
tt.Key(0, ui.KeyDown)
tt.Key(0, ui.KeyDown)
if v != 1e12-1 {
t.Errorf("stepper %v: two Downs gave %.0f", stepper, v)
}
v = 0.2
tt = numberInputStepTester(&v, NumberInputOptions{Min: 0, Max: 0.29999999995, Step: 0.1, StepperOnly: stepper})
tt.Key(0, ui.KeyUp)
if v != 0.2 {
t.Errorf("stepper %v: Up past Max 0.29999999995 gave %v", stepper, v)
}
tt.Key(0, ui.KeyEnd)
if stepper && v != 0.2 {
t.Errorf("End went to %v, past the largest value at precision 1", v)
}
}
}
+36 -21
View File
@@ -36,8 +36,9 @@ type NumberInputOptions struct {
// numberInputSpec is NumberInputOptions resolved.
type numberInputSpec struct {
lo, hi, step float64
decimals int
// lo and hi are the bounds on the precision grid; min and max the original ones.
lo, hi, min, max, step float64
decimals int
}
func (s numberInputSpec) format(v float64) string {
@@ -47,13 +48,18 @@ func (s numberInputSpec) format(v float64) string {
return strconv.FormatFloat(v, 'f', s.decimals, 64)
}
// clamp rounds v to the precision, then bounds it; the bounds sit on that grid.
func (s numberInputSpec) clamp(v float64) float64 {
// round rounds v to the precision.
func (s numberInputSpec) round(v float64) float64 {
p := math.Pow(10, float64(s.decimals))
return math.Max(s.lo, math.Min(s.hi, math.Round(v*p)/p)) + 0 // + 0 turns -0 into 0
return math.Round(v*p)/p + 0 // + 0 turns -0 into 0
}
// stepBy moves v n steps along the grid of Step from Min, stopping at the bounds.
// clamp rounds v to the precision, then bounds it; the bounds sit on that grid.
func (s numberInputSpec) clamp(v float64) float64 {
return math.Max(s.lo, math.Min(s.hi, s.round(v)))
}
// stepBy moves v n grid steps from Min, exactly checked against the original bounds.
func (s numberInputSpec) stepBy(v float64, n int) float64 {
if math.IsNaN(v) {
// From empty, the first step lands next to zero.
@@ -63,21 +69,25 @@ func (s numberInputSpec) stepBy(v float64, n int) float64 {
if math.IsInf(base, 0) {
base = 0
}
k := (v - base) / s.step
if n > 0 {
k = numberInputFloor(k) + float64(n)
} else {
k = numberInputCeil(k) + float64(n)
at := func(k float64) float64 { return s.round(base + k*s.step) }
k := math.Round((v - base) / s.step)
// Off the grid, the nearest point in the direction is the first step.
if n > 0 && at(k) <= v || n < 0 && at(k) >= v {
k += float64(n)
}
return s.clamp(base + k*s.step)
r := at(k)
switch {
case r > s.max:
r = at(k - 1)
case r < s.min:
r = at(k + 1)
}
if r > s.max || r < s.min {
return v
}
return r
}
// numberInputFloor floors x, forgiving floating-point error just below an integer.
func numberInputFloor(x float64) float64 { return math.Floor(x + math.Max(1e-9, 1e-12*math.Abs(x))) }
// numberInputCeil ceils x, forgiving floating-point error just above an integer.
func numberInputCeil(x float64) float64 { return math.Ceil(x - math.Max(1e-9, 1e-12*math.Abs(x))) }
// numberInputSame reports equal values, counting NaN (empty) as equal.
func numberInputSame(a, b float64) bool { return a == b || math.IsNaN(a) && math.IsNaN(b) }
@@ -103,10 +113,15 @@ func numberInputResolve(opts NumberInputOptions) numberInputSpec {
s.decimals = len(t) - strings.IndexByte(t, '.') - 1
}
}
// Bounds move inward onto the precision grid, so clamped values keep both.
// Bounds move inward onto the precision grid, compared exactly with the originals.
s.min, s.max = s.lo, s.hi
p := math.Pow(10, float64(s.decimals))
s.lo = numberInputCeil(s.lo*p)/p + 0 // + 0 turns -0 into 0
s.hi = numberInputFloor(s.hi*p)/p + 0
if s.lo = s.round(s.lo); s.lo < s.min {
s.lo = s.round(s.lo + 1/p)
}
if s.hi = s.round(s.hi); s.hi > s.max {
s.hi = s.round(s.hi - 1/p)
}
if s.lo > s.hi {
panic("mujica: NumberInput has no value in range at this precision")
}
+5 -5
View File
@@ -23,13 +23,13 @@ func TestPasswordInputMaskPasteAndReveal(t *testing.T) {
tt.Command("paste")
tt.Type("et")
if pw != "s3cret" || changes != 2 {
t.Fatalf("value %q changes %d", pw, changes)
t.Fatalf("value matched %v (length %d), changes %d", pw == "s3cret", len(pw), changes)
}
tt.SetClipboard("")
tt.Command("selectAll")
tt.Command("copy")
if tt.Clipboard() != "" {
t.Fatalf("masked field copied %q", tt.Clipboard())
t.Fatalf("masked field copied %d characters", len(tt.Clipboard()))
}
for _, s := range tt.Texts() {
if s == "s3cret" {
@@ -40,13 +40,13 @@ func TestPasswordInputMaskPasteAndReveal(t *testing.T) {
t.Fatal(err)
}
if _, ok := tt.Find("Hide password"); !ok || pw != "s3cret" {
t.Fatalf("after reveal: value %q, texts %q", pw, tt.Texts())
t.Fatalf("after reveal: value matched %v, hide toggle shown %v", pw == "s3cret", ok)
}
_ = tt.Click("Password")
tt.Command("selectAll")
tt.Command("copy")
if tt.Clipboard() != "s3cret" {
t.Fatalf("revealed field copied %q", tt.Clipboard())
t.Fatalf("revealed copy matched %v (length %d)", tt.Clipboard() == "s3cret", len(tt.Clipboard()))
}
tt.Key(0, ui.KeyEnter)
if submits != 1 {
@@ -82,7 +82,7 @@ func TestPasswordInputDisabled(t *testing.T) {
_ = tt.Click("Off")
tt.Type("zz")
if pw != "locked" {
t.Fatalf("disabled value %q", pw)
t.Fatalf("disabled value matched %v (length %d)", pw == "locked", len(pw))
}
if _, ok := tt.Find("Show password"); !ok {
t.Fatal("disabled toggle revealed")