Choices: review r2 fixes
This commit is contained in:
10 files changed
+300
-54
No files matched your search
+4
-3
@@ -79,7 +79,7 @@ func Cascader(c *ui.Context, path *[]string, opts CascaderOptions) ChoiceResult
|
||||
if opts.Disabled {
|
||||
st.open = false
|
||||
}
|
||||
if tr.Clicked() || (!st.open && tr.Shortcut(0, ui.KeyDown)) {
|
||||
if tr.Clicked() || (!st.open && tr.Shortcut(0, ui.KeyDown) && !tr.IsDisabled()) {
|
||||
st.open = !st.open
|
||||
if st.open {
|
||||
st.active = slices.Clone(*path)
|
||||
@@ -170,10 +170,11 @@ func cascaderColumn(c *ui.Context, st *cascaderState, path *[]string, opts Casca
|
||||
selectNote(c, msg(c, "cascader.empty"))
|
||||
default:
|
||||
ui.Scroll(c).MaxHeight(260).Padding(4).Children(func() {
|
||||
entry := slices.IndexFunc(lv.Nodes, func(n CascaderNode) bool { return !n.Disabled })
|
||||
for i, n := range lv.Nodes {
|
||||
on := depth < len(st.active) && st.active[depth] == n.Value
|
||||
deeper = deeper || on && !n.Leaf
|
||||
cascaderRow(c, st, path, opts, prefix, n, on, i == 0)
|
||||
cascaderRow(c, st, path, opts, prefix, n, on, i == entry)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -208,7 +209,7 @@ func cascaderRow(c *ui.Context, st *cascaderState, path *[]string, opts Cascader
|
||||
st.active, *path, st.open = full, full, false
|
||||
case clicked:
|
||||
open(false)
|
||||
case b.Shortcut(0, ui.KeyRight) && !n.Leaf:
|
||||
case b.Shortcut(0, ui.KeyRight) && !n.Leaf && !b.IsDisabled():
|
||||
open(true)
|
||||
case b.Shortcut(0, ui.KeyLeft) && depth > 0:
|
||||
st.focus, st.focusSet = slices.Clone(prefix), true
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
// Keys delivered in the frame a control or option turns disabled must change nothing.
|
||||
|
||||
func TestMultiSelectBackspaceAsItDisables(t *testing.T) {
|
||||
values, disabled := []int{1, 2}, false
|
||||
tt := choiceBetween(func(c *ui.Context) {
|
||||
MultiSelect(c, &values, multiSelectTestOptions, MultiSelectOptions{Disabled: disabled, Label: "Flowers"})
|
||||
})
|
||||
_ = tt.Click("Before")
|
||||
tt.Key(0, ui.KeyTab)
|
||||
if !tt.Focused("Flowers") {
|
||||
t.Fatal("field not focused")
|
||||
}
|
||||
disabled = true
|
||||
tt.Key(0, ui.KeyBackspace)
|
||||
if !slices.Equal(values, []int{1, 2}) {
|
||||
t.Fatalf("Backspace removed a value: %v", values)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComboboxEnterAsItDisables(t *testing.T) {
|
||||
value, disabled := "", false
|
||||
tt := choiceBetween(func(c *ui.Context) {
|
||||
Combobox(c, &value, comboboxTestFonts, ComboboxOptions{AllowCustom: true, Disabled: disabled, Label: "Font"})
|
||||
})
|
||||
_ = tt.Click("Before")
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Type("Caslon")
|
||||
tt.Key(0, ui.KeyEscape)
|
||||
disabled = true
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if value != "" {
|
||||
t.Fatalf("Enter submitted %q", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCascaderRightAsRowDisables(t *testing.T) {
|
||||
var path []string
|
||||
f := newCascaderFake()
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
Cascader(c, &path, CascaderOptions{Level: f.level, Load: f.load, AnyLevel: true, Label: "Place"})
|
||||
}, 700, 500)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyDown)
|
||||
if !tt.Focused("Europe") {
|
||||
t.Fatal("Europe not focused")
|
||||
}
|
||||
roots := f.levels[""]
|
||||
roots.Nodes = slices.Clone(roots.Nodes)
|
||||
roots.Nodes[0].Disabled = true
|
||||
f.levels[""] = roots
|
||||
tt.Key(0, ui.KeyRight)
|
||||
if len(path) != 0 || len(f.loads) != 0 {
|
||||
t.Fatalf("Right on a disabled row chose %v, loaded %v", path, f.loads)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectEnterOnOptionAsItDisables(t *testing.T) {
|
||||
v := "apple"
|
||||
options := slices.Clone(selectTestOptions)
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
Select(c, &v, options, SelectOptions{Label: "Fruit"})
|
||||
}, 400, 400)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyDown)
|
||||
tt.Key(0, ui.KeyDown)
|
||||
options[1].Disabled = true
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if v != "apple" {
|
||||
t.Fatalf("Enter chose the disabled option: %q", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntryFocusSkipsDisabledFirstRow(t *testing.T) {
|
||||
var path []string
|
||||
f := &cascaderFake{levels: map[string]CascaderLevel{
|
||||
"": {Nodes: []CascaderNode{{Value: "aq", Label: "Antarctica", Disabled: true}, {Value: "eu", Label: "Europe", Leaf: true}}},
|
||||
}}
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
Cascader(c, &path, CascaderOptions{Level: f.level, Load: f.load, Label: "Place"})
|
||||
}, 700, 500)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyDown)
|
||||
if !tt.Focused("Europe") {
|
||||
t.Fatal("cascader entry did not reach the first enabled row")
|
||||
}
|
||||
nodes := []TreeSelectNode{{ID: "a", Label: "Archived", Disabled: true}, {ID: "b", Label: "Brass"}, {ID: "c", Label: "Copper"}}
|
||||
var sel []string
|
||||
tt = ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
TreeSelect(c, &sel, nodes, TreeSelectOptions{Label: "Metal"})
|
||||
}, 500, 500)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
tt.Key(0, ui.KeyDown)
|
||||
if !tt.Focused("Brass") {
|
||||
t.Fatal("tree entry did not reach the first enabled row")
|
||||
}
|
||||
tt.Key(0, ui.KeyDown)
|
||||
if !tt.Focused("Copper") {
|
||||
t.Fatal("arrows stopped working after entry")
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -127,7 +127,10 @@ func Combobox(c *ui.Context, value *string, options []string, opts ComboboxOptio
|
||||
comboboxStatus(c, f.Bounds(), opts.Loading)
|
||||
}
|
||||
focused := in.Focused()
|
||||
if v, ok := p.Chosen(); ok {
|
||||
if in.IsDisabled() {
|
||||
// Input delivered as the field turned disabled commits nothing.
|
||||
*query = *value
|
||||
} else if v, ok := p.Chosen(); ok {
|
||||
*value, *query = v, v
|
||||
*gen, *refocus = *gen+1, focused
|
||||
} else if in.Submitted() && opts.AllowCustom {
|
||||
|
||||
@@ -45,5 +45,6 @@ ui.Render(view, 400, 300, 1)
|
||||
## 限制
|
||||
|
||||
- 基于 `ui.SliderBase`,它向辅助技术报告 Slider 角色、范围与当前值(ui/base.go:128)。
|
||||
- MyGo 只把方向键、Home、End 交给 `SliderBase` 本身(ui/base.go:118,按键路由见 ui/input.go:495),并固定以范围的 1% 移动。本组件让它操作副本,按方向判断按键后自己按 `Step` 移动;距端点不足 1% 时 MyGo 的方向键与 Home/End 结果相同,组件改看松开的是哪个键(`HandleInput` 能收到 KeyUp),在键松开前不改值。
|
||||
- MyGo 只把方向键、Home、End 交给 `SliderBase` 本身(ui/base.go:118,按键路由见 ui/input.go:495),以范围的 1% 移动并把结果舍入到 1e-9(ui/widgets.go:375)。本组件让它操作副本,按方向判断按键后自己按 `Step` 移动。两种情况下按键结果无法区分:距端点不足 1% 时方向键与 Home/End 结果相同;步长细到 MyGo 舍入后不动。这时组件改看松开的是哪个键(`HandleInput` 能收到 KeyUp),在键松开时才改值;按住不放也只移动一步。
|
||||
- 步长网格复用 NumberInput 的实现:值为 `Min + n·Step`,小数位取 `Min` 与 `Step` 所需,端点跳转(End/Home、另一端)先对齐网格再写值并触发事件;例如 `Max=10, Step=3` 时 End 得到 9。
|
||||
- `SliderBase` 在构建时就处理指针(ui/base.go:111),禁用时外层先禁用,且只操作副本,保证值不变。
|
||||
@@ -37,7 +37,7 @@ items := []mujica.ToggleItem[string]{
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.ToggleGroup(c, &align, items, mujica.ToggleGroupOptions{Label: "Align"})
|
||||
}`, Keys: "Tab enters the group at the pressed item; Left/Right (Up/Down when vertical), Home and End move; Space presses."},
|
||||
}`, Keys: "Tab enters the group at the first enabled item, later at the last focused one; Left/Right (Up/Down when vertical), Home and End move; Space presses."},
|
||||
{ID: "034", Name: "Slider", ZhName: "滑块", Category: choiceCategory, Demo: sliderDemo, Code: `volume := 40.0
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
|
||||
+2
-2
@@ -44,11 +44,11 @@ func MultiSelect[T comparable](c *ui.Context, values *[]T, options []SelectOptio
|
||||
*open = false
|
||||
}
|
||||
query := ui.Local(tr, multiSelectKey{"query"}, func() string { return "" })
|
||||
if tr.Clicked() || (!*open && tr.Shortcut(0, ui.KeyDown)) {
|
||||
if tr.Clicked() || (!*open && tr.Shortcut(0, ui.KeyDown) && !tr.IsDisabled()) {
|
||||
*open = !*open
|
||||
*query = ""
|
||||
}
|
||||
if n := len(*values); n > 0 && tr.Shortcut(0, ui.KeyBackspace) {
|
||||
if n := len(*values); n > 0 && tr.Shortcut(0, ui.KeyBackspace) && !tr.IsDisabled() {
|
||||
*values = slices.Clone((*values)[:n-1])
|
||||
}
|
||||
selectField(c, tr, *open, func() {
|
||||
|
||||
@@ -45,11 +45,18 @@ type selectTypeahead struct {
|
||||
func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], opts SelectOptions) ChoiceResult {
|
||||
k := Tokens(c)
|
||||
old := *value
|
||||
value = choiceTarget(value, opts.Disabled)
|
||||
// SelectBase chooses into a copy as it is built; a choice counts once the option is known enabled.
|
||||
pick := old
|
||||
var s *ui.SelectParts[T]
|
||||
// SelectBase takes clicks as it is built: a disabled wrapper and a copy block them.
|
||||
ui.Row(c).Shrink(0).Disabled(opts.Disabled).Children(func() { s = ui.SelectBase(c, value) })
|
||||
ui.Row(c).Shrink(0).Disabled(opts.Disabled).Children(func() { s = ui.SelectBase(c, &pick) })
|
||||
tr := s.Trigger
|
||||
commit := func() {
|
||||
if pick != *value && !tr.IsDisabled() && selectEnabled(options, pick) {
|
||||
*value = pick
|
||||
}
|
||||
pick = *value
|
||||
}
|
||||
commit()
|
||||
if opts.Label != "" {
|
||||
tr.Label(opts.Label)
|
||||
}
|
||||
@@ -67,7 +74,7 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op
|
||||
})
|
||||
if ta.pending {
|
||||
ta.pending = false
|
||||
if !s.Open() {
|
||||
if !s.Open() && !tr.IsDisabled() {
|
||||
if i := selectFind(options, *value, ta.prefix); i >= 0 {
|
||||
*value = options[i].Value
|
||||
}
|
||||
@@ -75,7 +82,7 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op
|
||||
}
|
||||
var zero T
|
||||
label, has := selectLabel(options, *value)
|
||||
if opts.Clearable && has && (tr.Shortcut(0, ui.KeyBackspace) || tr.Shortcut(0, ui.KeyDelete)) {
|
||||
if opts.Clearable && has && (tr.Shortcut(0, ui.KeyBackspace) || tr.Shortcut(0, ui.KeyDelete)) && !tr.IsDisabled() {
|
||||
*value, has = zero, false
|
||||
}
|
||||
selectField(c, tr, s.Open(), func() {
|
||||
@@ -88,6 +95,7 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op
|
||||
*value = zero
|
||||
}
|
||||
})
|
||||
pick = *value
|
||||
lastHi := ui.Local(tr, selectKey{"shown"}, func() int { return -1 })
|
||||
if opts.Disabled {
|
||||
// SelectParts has no way to close it (ui/base.go:310): a disabled select builds no popup.
|
||||
@@ -113,6 +121,7 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op
|
||||
}
|
||||
})
|
||||
})
|
||||
commit()
|
||||
if !s.Open() {
|
||||
*lastHi = -1
|
||||
}
|
||||
@@ -123,6 +132,16 @@ func Select[T comparable](c *ui.Context, value *T, options []SelectOption[T], op
|
||||
return ChoiceResult{tr, changed}
|
||||
}
|
||||
|
||||
// selectEnabled reports whether v is an option that is not disabled.
|
||||
func selectEnabled[T comparable](options []SelectOption[T], v T) bool {
|
||||
for _, o := range options {
|
||||
if o.Value == v {
|
||||
return !o.Disabled
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// selectFind returns the enabled option matching typed prefix.
|
||||
func selectFind[T comparable](options []SelectOption[T], current T, prefix string) int {
|
||||
start := 0
|
||||
|
||||
@@ -40,9 +40,10 @@ const (
|
||||
sliderMarkH float32 = 18
|
||||
)
|
||||
|
||||
// sliderGeom maps values to x positions across a slider's box.
|
||||
// sliderGeom maps values to x positions across a slider's box; grid holds the step points.
|
||||
type sliderGeom struct {
|
||||
lo, hi, step float64
|
||||
grid numberInputSpec
|
||||
trackY float32
|
||||
}
|
||||
|
||||
@@ -51,23 +52,28 @@ func (g sliderGeom) x(r ui.Rect, v float64) float32 {
|
||||
return r.X + sliderThumb/2 + (r.W-sliderThumb)*max(0, min(1, f))
|
||||
}
|
||||
|
||||
// snap rounds v to the nearest step inside the range.
|
||||
// snap moves v to the nearest step point inside the range.
|
||||
func (g sliderGeom) snap(v float64) float64 {
|
||||
v = max(g.lo, min(g.hi, v))
|
||||
if g.step <= 0 {
|
||||
return v
|
||||
}
|
||||
v = g.lo + math.Round((v-g.lo)/g.step)*g.step
|
||||
if v > g.hi {
|
||||
v -= g.step
|
||||
}
|
||||
return math.Round(v*1e9) / 1e9
|
||||
return g.grid.stepBy(v, 0)
|
||||
}
|
||||
|
||||
// sliderKey is the last key released on a slider, and a move waiting for it.
|
||||
// move goes n steps from v, 1% of the range each when continuous, staying in range.
|
||||
func (g sliderGeom) move(v float64, n int) float64 {
|
||||
if g.step <= 0 {
|
||||
return max(g.lo, min(g.hi, v+float64(n)*(g.hi-g.lo)/100))
|
||||
}
|
||||
return g.grid.stepBy(v, n)
|
||||
}
|
||||
|
||||
// sliderKey is the last key released on a slider, a move waiting for it, and whether its press moved.
|
||||
type sliderKey struct {
|
||||
up ui.Key
|
||||
pending int
|
||||
acted bool
|
||||
}
|
||||
|
||||
type sliderKeyID struct{}
|
||||
@@ -85,46 +91,69 @@ func sliderControl(c *ui.Context, v float64, g sliderGeom, from, to float64) (s
|
||||
ks.up = ev.Key
|
||||
return true
|
||||
})
|
||||
up, dir := ks.up, ks.pending
|
||||
up, pending, acted := ks.up, ks.pending, ks.acted
|
||||
ks.up, ks.pending = 0, 0
|
||||
if up != 0 {
|
||||
ks.acted = false
|
||||
}
|
||||
if _, _, dragged = s.Dragged(); dragged {
|
||||
return s, max(from, min(to, g.snap(raw))), true
|
||||
}
|
||||
unsure := dir != 0
|
||||
dir, jump, known := 0, false, true
|
||||
switch {
|
||||
case raw < v:
|
||||
dir, unsure = -1, false
|
||||
case raw > v:
|
||||
dir, unsure = 1, false
|
||||
case raw != v:
|
||||
// MyGo moved the copy 1% (an arrow) or to an end (Home, End), rounded to 1e-9 (ui/widgets.go:375).
|
||||
dir = 1
|
||||
if raw < v {
|
||||
dir = -1
|
||||
}
|
||||
end := g.lo
|
||||
if dir > 0 {
|
||||
end = g.hi
|
||||
}
|
||||
arrow := max(g.lo, min(g.hi, v+float64(dir)*(g.hi-g.lo)/100))
|
||||
jump = math.Abs(raw-end) < math.Abs(raw-arrow)
|
||||
if math.Abs(arrow-end) <= 1e-9+math.Abs(end)*1e-12 {
|
||||
// Near an end an arrow and Home/End agree: the released key tells which it was.
|
||||
dir, jump, known = sliderKeyMove(up, dir)
|
||||
}
|
||||
if up == 0 {
|
||||
ks.acted = true
|
||||
}
|
||||
case pending != 0:
|
||||
dir, jump, known = sliderKeyMove(up, pending)
|
||||
case up != 0 && !acted:
|
||||
// A step finer than MyGo's rounding leaves the copy still: act on the release.
|
||||
dir, jump, known = sliderKeyMove(up, 0)
|
||||
}
|
||||
if dir == 0 {
|
||||
if !known {
|
||||
ks.pending = dir
|
||||
return s, v, false
|
||||
}
|
||||
end, limit := g.lo, from
|
||||
if dir > 0 {
|
||||
end, limit = g.hi, to
|
||||
switch {
|
||||
case dir == 0:
|
||||
return s, v, false
|
||||
case jump && dir < 0:
|
||||
return s, g.snap(from), false
|
||||
case jump:
|
||||
return s, g.snap(to), false
|
||||
}
|
||||
jump := raw == end
|
||||
// An arrow within 1% of an end lands on it too: the released key tells which it was.
|
||||
if unsure || jump && math.Abs(v-end) <= (g.hi-g.lo)/100*(1+1e-9) {
|
||||
switch up {
|
||||
case ui.KeyHome, ui.KeyEnd:
|
||||
jump = true
|
||||
case ui.KeyLeft, ui.KeyRight, ui.KeyUp, ui.KeyDown:
|
||||
jump = false
|
||||
default:
|
||||
ks.pending = dir
|
||||
return s, v, false
|
||||
}
|
||||
return s, max(from, min(to, g.move(v, dir))), false
|
||||
}
|
||||
|
||||
// sliderKeyMove reads a released key as direction and jump; an unknown key keeps dir, unresolved.
|
||||
func sliderKeyMove(up ui.Key, dir int) (int, bool, bool) {
|
||||
switch up {
|
||||
case ui.KeyLeft, ui.KeyDown:
|
||||
return -1, false, true
|
||||
case ui.KeyRight, ui.KeyUp:
|
||||
return 1, false, true
|
||||
case ui.KeyHome:
|
||||
return -1, true, true
|
||||
case ui.KeyEnd:
|
||||
return 1, true, true
|
||||
}
|
||||
if jump {
|
||||
return s, limit, false
|
||||
}
|
||||
step := g.step
|
||||
if step <= 0 {
|
||||
step = (g.hi - g.lo) / 100
|
||||
}
|
||||
return s, max(from, min(to, g.snap(v+float64(dir)*step))), false
|
||||
return dir, false, dir == 0
|
||||
}
|
||||
|
||||
func sliderSetup(min, max, step float64, marks []SliderMark, showValue bool, c *ui.Context) (sliderGeom, float32) {
|
||||
@@ -135,6 +164,10 @@ func sliderSetup(min, max, step float64, marks []SliderMark, showValue bool, c *
|
||||
panic("mujica: slider needs Min < Max and Step >= 0")
|
||||
}
|
||||
g := sliderGeom{lo: min, hi: max, step: step}
|
||||
if step > 0 {
|
||||
// NumberInput's grid: exact bounds and integer step indexes at the decimals Min and Step need.
|
||||
g.grid = numberInputResolve(NumberInputOptions{Min: min, Max: max, Step: step, Precision: sliderDecimals(min, step)})
|
||||
}
|
||||
h := Density(c).ControlHeight()
|
||||
g.trackY = h / 2
|
||||
if showValue {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
// sliderRecord builds a Slider and a RangeSlider and records the values each Changed reported.
|
||||
type sliderRecord struct {
|
||||
v, low, high float64
|
||||
single, lows, his []float64
|
||||
}
|
||||
|
||||
func sliderGridTester(r *sliderRecord, min, max, step float64) *ui.Tester {
|
||||
return ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
ui.Column(c).Width(216).Gap(10).Children(func() {
|
||||
if Slider(c, &r.v, SliderOptions{Min: min, Max: max, Step: step, Label: "One"}).Changed() {
|
||||
r.single = append(r.single, r.v)
|
||||
}
|
||||
if RangeSlider(c, &r.low, &r.high, RangeSliderOptions{Min: min, Max: max, Step: step, Label: "Two"}).Changed() {
|
||||
r.lows, r.his = append(r.lows, r.low), append(r.his, r.high)
|
||||
}
|
||||
})
|
||||
}, 300, 300)
|
||||
}
|
||||
|
||||
func TestSliderEndSnapsToStepGrid(t *testing.T) {
|
||||
r := &sliderRecord{v: 3, low: 0, high: 3}
|
||||
tt := sliderGridTester(r, 0, 10, 3)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyEnd)
|
||||
if len(r.single) != 1 || r.single[0] != 9 || r.v != 9 {
|
||||
t.Fatalf("Slider End reported %v, value %v", r.single, r.v)
|
||||
}
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyEnd)
|
||||
if len(r.his) != 1 || r.his[0] != 9 || r.high != 9 {
|
||||
t.Fatalf("RangeSlider End reported %v, high %v", r.his, r.high)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliderTinyStep(t *testing.T) {
|
||||
r := &sliderRecord{v: 0, low: 0, high: 1e-9}
|
||||
tt := sliderGridTester(r, 0, 1e-9, 1e-10)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyRight)
|
||||
if r.v != 1e-10 {
|
||||
t.Fatalf("Slider Right: %v", r.v)
|
||||
}
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyRight)
|
||||
if r.low != 1e-10 {
|
||||
t.Fatalf("RangeSlider Right: %v", r.low)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliderKeepsPreciseMin(t *testing.T) {
|
||||
const lo = 0.1234567891
|
||||
r := &sliderRecord{v: 0.5, low: 0.5, high: 0.6}
|
||||
tt := sliderGridTester(r, lo, 1, 0.1)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyHome)
|
||||
if r.v < lo || r.v != lo {
|
||||
t.Fatalf("Slider Home: %v, Min %v", r.v, lo)
|
||||
}
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyHome)
|
||||
if r.low < lo || r.low != lo {
|
||||
t.Fatalf("RangeSlider Home: %v, Min %v", r.low, lo)
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -74,7 +74,7 @@ func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts
|
||||
if opts.Disabled {
|
||||
st.open = false
|
||||
}
|
||||
if tr.Clicked() || (!st.open && tr.Shortcut(0, ui.KeyDown)) {
|
||||
if tr.Clicked() || (!st.open && tr.Shortcut(0, ui.KeyDown) && !tr.IsDisabled()) {
|
||||
st.open, st.query = !st.open, ""
|
||||
}
|
||||
labels := map[string]string{}
|
||||
@@ -111,8 +111,9 @@ func TreeSelect(c *ui.Context, selected *[]string, nodes []TreeSelectNode, opts
|
||||
}
|
||||
ui.Scroll(c).MaxHeight(300).Padding(4).Children(func() {
|
||||
ui.Column(c).FocusGroup(ui.Vertical).Children(func() {
|
||||
entry := slices.IndexFunc(rows, func(r treeSelectRow) bool { return !r.node.Disabled })
|
||||
for i, r := range rows {
|
||||
treeSelectRowView(c, st, selected, r, i == 0, shown, opts)
|
||||
treeSelectRowView(c, st, selected, r, i == entry, shown, opts)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -287,7 +288,7 @@ func treeSelectRowView(c *ui.Context, st *treeSelectState, selected *[]string, r
|
||||
b.Focus()
|
||||
st.focus = ""
|
||||
}
|
||||
right, left := b.Shortcut(0, ui.KeyRight), b.Shortcut(0, ui.KeyLeft)
|
||||
right, left := b.Shortcut(0, ui.KeyRight) && !b.IsDisabled(), b.Shortcut(0, ui.KeyLeft) && !b.IsDisabled()
|
||||
switch {
|
||||
case right && branch && !open:
|
||||
st.expanded[n.ID] = true
|
||||
|
||||
Reference in new issue
Block a user