Merge branch 'worktree-agent-a085d9a07ef787c18'
This commit is contained in:
4 files changed
+68
-27
No files matched your search
@@ -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)。
|
||||
|
||||
|
||||
@@ -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
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user