Audit keyboard, focus, veils, motion and clocks across all 500
A cross-cutting pass, each fix with a test that failed first. Veils: MyGo still hands a key to a base that turned off this frame, so every element key now goes through pixel.Shortcut, which an archtest enforces. Input handlers record and the build applies them only while enabled; the editor turns away a veil-frame letter or copy; value bases get a copy and keep only enabled turns. Popups give a held focus back to their opener when the veil lifts. Motion: S5 is enforced at the clock. Every Tick, Since, Step, Toward and Shot runs at 4-15 fps; blinks keep 1 Hz on 4 fps steps, fast animations step at 15, and drawn time is quantized to frames. Clocks stepping back: Since holds at zero without overflow, and k.Elapsed moves a stored start to now, so nothing goes negative, runs backward or indexes out of range. Also: logs count typed text instead of holding it, gallery names use glyphs the font has, removable rows are keyed, components take their own Options (archtest), and sweeps over all 508 demos check veiled keys, clicks, wheels, the clock stepping back and names.
This commit is contained in:
198 files changed
+2708
-422
No files matched your search
@@ -53,28 +53,29 @@ Imports point down only. Same-tier imports are forbidden. `internal/archtest` en
|
||||
|
||||
## Component rules
|
||||
|
||||
- Signatures and results: PRD §5.1. Zero options give the default look. Sizes in options are dots.
|
||||
- Signatures and results: PRD §5.1, held by archtest `TestComponentsTakeTheirOptions`: `Name` takes `NameOptions` and returns `NameResult`. Zero options give the default look; required values are parameters. Sizes in options are dots.
|
||||
- `k := pixel.K(c)` once per component; layout sizes through `k.D(n)`. Integer options default through `pixel.Or`.
|
||||
- Custom-drawn controls end their Draw with `g.Finish(st)`. Grids of cells moved by arrows use `pixel.Pad`; custom-drawn sliders use `input.SlideBase`; one-shot timing steps with `k.Since`; a decorative one-shot on a cue steps with `k.Shot`, which Reduce Motion spends whole; a view that changes at a known moment asks `k.At`; a two-way slide (bars, fades, wipes) steps with `k.Toward`, which turns from where it is.
|
||||
- Custom-drawn controls end their Draw with `g.Finish(st)`. Grids of cells moved by arrows use `pixel.Pad`; custom-drawn sliders use `input.SlideBase`; every clock runs at S5's 4–15 fps (`checkFPS` panics otherwise): a blink is `g.Step(4)%4 < 2`, a timer counts quarter seconds, and a view that changes on whole seconds asks `k.At`; one-shot timing steps with `k.Since`, which holds at zero when the clock steps back; a stored start is read through `k.Elapsed(&t)`, which moves it to now when the clock steps back; drawing from time quantizes it to whole frames (`Truncate(time.Second / 15)`); a decorative one-shot on a cue steps with `k.Shot`, which Reduce Motion spends whole; a view that changes at a known moment asks `k.At`; a two-way slide (bars, fades, wipes) steps with `k.Toward`, which turns from where it is.
|
||||
- A one-shot effect inside a lasting view starts when its cue changes (a `Cue` option read through `k.Cue`, or a value that rises), never on its first frame. A state screen (`ErrorState`, `SuccessState`) starts when it appears. Decorative motion jumps to its end under Reduce Motion; countdowns and loaders keep time.
|
||||
- Containers take content as a final `func()`; interactive results are `NameResult`.
|
||||
- A Clicked, Shortcut, Changed or drag during the build makes MyGo build the frame again, so what they change shows at once. State changed any other way after its element was built (`Pressed`, `Edit.Changed`, a drag's release) is read at paint time, or set on the element after its children.
|
||||
- Never call `Selected`. The pixel theme keeps MyGo's row highlights clear, since scrolled rows sit between dots: draw chosen rows yourself.
|
||||
- A disabled or inert scroll view must not move: cover it, so the wheel stops above it.
|
||||
- Read pointers and keys only while `base.IsDisabled()` is false, never by the `Disabled` option alone: `pixel.Shell` disables the base under Inert too, and a drag begun before stays pressed.
|
||||
- Read pointers and keys only while `base.IsDisabled()` is false, never by the `Disabled` option alone: `pixel.Shell` disables the base under Inert too, and a drag begun before stays pressed. MyGo still hands a key to a base that turned off this frame, so keys go through `pixel.Shortcut` (archtest bans an element's own `Shortcut`); a container that stays enabled, or a wrapper around an editor, also checks its own off state. `pixel.Editor` turns away text typed in a veil's first frame; read an edited value after building its editor, or gate the read. A `HandleInput` handler runs before the build learns the base is off: it records, and the build applies only while the base is enabled.
|
||||
- MyGo's value bases (`ToggleBase`, `SwitchBase`, `CheckboxBase`, `RadioBase`, `SegmentedBase`, `TabsBase`, `CollapsibleBase`) turn their value while built, before `Disabled` applies, so a key in a veil's first frame lands. Hand them a copy; keep only turns of an enabled base.
|
||||
- MyGo paints a disabled element and all inside it at half opacity, and only a disabled element reads as disabled to assistive technology. Disable only drawless bases, as `pixel.Shell` does. A container with drawn content stays enabled, parks its focus, ignores input and says `DISABLED` in its description.
|
||||
- A press focuses the nearest focusable element under it, or nothing, and a focusable element keeps the click: clicks never reach its ancestors. A control that acts on a press is that focusable element. A `DialogBase` moves the focus inside as it opens and keeps Tab there, but only onto something focusable: make its panel focusable, ring off, and build its children in `pixel.Harbor(c, panel, …)` so controls turning off park their focus there and get it back. A closing overlay hands the focus back to its opener. While a press is on, `Hovered` is false for every other element: ask `Pressed`.
|
||||
- Enter and Space click a focused clickable element unless a `Shortcut` takes them. Take them wherever a click reads the pointer's position.
|
||||
- Under `pixel.Settings.Inert` (a busy veil) `pixel.Shell` disables every base and gives the focus back after. A popup sits above any veil: a control closes its popup when disabled or inert.
|
||||
- Elements are known by their place among siblings. Wrap one whose earlier siblings come and go in `ui.Box(c).Key(k)`, or it loses focus and state.
|
||||
- Under `pixel.Settings.Inert` (a busy veil) `pixel.Shell` disables every base and gives the focus back after. A popup sits above any veil: a control closes its popup when disabled or inert. If the popup held the focus, focus the opener and `pixel.Park` it in that frame; tell it by `Focused` on the popup's controls, since MyGo links parents only after a frame.
|
||||
- Elements are known by their place among siblings. Wrap one whose earlier siblings come and go in `ui.Box(c).Key(k)`, or it loses focus and state. Rows the caller removes are keyed by a name that must be unique: panic on twins.
|
||||
- Hosts send a key's release with its press, so tell a held key by its repeats.
|
||||
- Clip only boxes that sit on whole dots; split widths with whole-dot shares, not equal `Grow`. `Grow` starts from zero size, so never use it on a content-sized child.
|
||||
- `List` row builders run during layout: wrap them in `pixel.Lazy(c, row)`. A click met there lands after the build; keep it in `Local` state, invalidate, and report it next frame.
|
||||
- Single-key shortcuts only while `pixel.Typing(c)` is false; MyGo routes shortcuts before a focused field.
|
||||
- Copy shown by a component comes from `k.Msg`; business copy comes from the caller. Ark Pixel 12px lacks some hanzi: `TestMessagesHaveGlyphs` holds every message to the font.
|
||||
- Copy shown by a component comes from `k.Msg`; business copy comes from the caller. Ark Pixel 12px lacks some hanzi: `TestMessagesHaveGlyphs` holds every message to the font, `TestGalleryTextHasGlyphs` every gallery name and summary.
|
||||
- Item slots look alike everywhere: draw them with `data.DrawSlot` or `data.ItemSlot`.
|
||||
- Fail fast: panic on programmer errors such as bad options. Never swallow an error.
|
||||
- Composites log state changes: `slog.Debug("8bit: <component> <event>", …)`. Never log secrets.
|
||||
- Composites log state changes: `slog.Debug("8bit: <component> <event>", …)`. Never log secrets, nor anything typed or copied: log counts. The gallery logs which demo spoke, not what it said.
|
||||
- No cross-frame `*ui.Element` caching. No goroutines touching elements.
|
||||
- Files under 500 lines. Comments: one line, ten words at most, only when needed. No dead code, no versioned APIs.
|
||||
|
||||
|
||||
+3
-4
@@ -13,19 +13,18 @@ type Credit struct {
|
||||
}
|
||||
|
||||
type AboutOptions struct {
|
||||
Version string
|
||||
Credits []Credit
|
||||
}
|
||||
|
||||
// About shows the title as a logo, version, then credits.
|
||||
func About(c *ui.Context, title string, o AboutOptions) *ui.Element {
|
||||
func About(c *ui.Context, title, version string, o AboutOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
if o.Version == "" {
|
||||
if version == "" {
|
||||
panic("account: About needs a version")
|
||||
}
|
||||
return ui.Column(c).Gap(k.D(4)).AlignItems(ui.Center).Role(ui.RoleGroup).Label(title).Children(func() {
|
||||
text.OutlineText(c, title, text.OutlineTextOptions{Slot: pixel.Accent, Scale: 3})
|
||||
pixel.Text(c, k.Msg("version")+" "+o.Version, pixel.TextOptions{Slot: pixel.Faint})
|
||||
pixel.Text(c, k.Msg("version")+" "+version, pixel.TextOptions{Slot: pixel.Faint})
|
||||
if len(o.Credits) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -75,23 +75,23 @@ func KeyBindings(c *ui.Context, text string, binds []Binding, o KeyBindingsOptio
|
||||
kb.waiting = false
|
||||
case kb.waiting:
|
||||
// Waiting, every key binds, arrows too; Escape gives up.
|
||||
if base.Shortcut(0, ui.KeyEscape) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyEscape) {
|
||||
kb.waiting = false
|
||||
}
|
||||
for key := range caps {
|
||||
if base.Shortcut(0, key) && kb.waiting {
|
||||
if pixel.Shortcut(base, 0, key) && kb.waiting {
|
||||
binds[kb.cur].Key, kb.waiting, res.Changed = key, false, true
|
||||
slog.Debug("8bit: key bindings", "label", text, "action", binds[kb.cur].Action, "key", caps[key])
|
||||
}
|
||||
}
|
||||
default:
|
||||
if base.Shortcut(0, ui.KeyUp) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyUp) {
|
||||
kb.cur = max(0, kb.cur-1)
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyDown) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyDown) {
|
||||
kb.cur = min(len(binds)-1, kb.cur+1)
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyEnter) || base.Shortcut(0, ui.KeySpace) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyEnter) || pixel.Shortcut(base, 0, ui.KeySpace) {
|
||||
kb.waiting = true
|
||||
}
|
||||
if base.Clicked() {
|
||||
|
||||
@@ -34,11 +34,11 @@ func OptionCycler(c *ui.Context, text string, options []string, selected *int, o
|
||||
n, old := len(options), *selected
|
||||
if !base.IsDisabled() {
|
||||
step := 0
|
||||
if base.Shortcut(0, ui.KeyLeft) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyLeft) {
|
||||
step--
|
||||
}
|
||||
// Enter and Space step on; taken, clicks come from pointers.
|
||||
right, enter, space := base.Shortcut(0, ui.KeyRight), base.Shortcut(0, ui.KeyEnter), base.Shortcut(0, ui.KeySpace)
|
||||
right, enter, space := pixel.Shortcut(base, 0, ui.KeyRight), pixel.Shortcut(base, 0, ui.KeyEnter), pixel.Shortcut(base, 0, ui.KeySpace)
|
||||
if right || enter || space {
|
||||
step++
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func PasswordGrid(c *ui.Context, text string, password *string, o PasswordGridOp
|
||||
if picked >= 0 && len(*password) < n {
|
||||
*password += symbols[picked : picked+1]
|
||||
}
|
||||
if !base.IsDisabled() && base.Shortcut(0, ui.KeyBackspace) && *password != "" {
|
||||
if !base.IsDisabled() && pixel.Shortcut(base, 0, ui.KeyBackspace) && *password != "" {
|
||||
*password = (*password)[:len(*password)-1]
|
||||
}
|
||||
base.Description(symbols[*cur : *cur+1])
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestPlayerSelectChooses(t *testing.T) {
|
||||
func TestAboutAndPatchNotesRead(t *testing.T) {
|
||||
tt := view(500, 400, newWorld(), func(c *ui.Context) {
|
||||
ui.Row(c).Children(func() {
|
||||
account.About(c, "GAME", account.AboutOptions{Version: "1.2", Credits: []account.Credit{{Role: "Art", Name: "Ann"}}})
|
||||
account.About(c, "GAME", "1.2", account.AboutOptions{Credits: []account.Credit{{Role: "Art", Name: "Ann"}}})
|
||||
account.PatchNotes(c, "Notes", []account.Patch{{Version: "v2", Added: []string{"Boats"}, Fixed: []string{"Rain"}}}, account.PatchNotesOptions{Width: 120})
|
||||
})
|
||||
})
|
||||
@@ -157,6 +157,6 @@ func TestAboutAndPatchNotesRead(t *testing.T) {
|
||||
t.Fatal("the ADDED tag is not Good")
|
||||
}
|
||||
mustPanic(t, "no version", func() {
|
||||
view(500, 400, newWorld(), func(c *ui.Context) { account.About(c, "G", account.AboutOptions{}) })
|
||||
view(500, 400, newWorld(), func(c *ui.Context) { account.About(c, "G", "", account.AboutOptions{}) })
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package account_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/account"
|
||||
)
|
||||
|
||||
func TestSessionFocusFollowsItsRow(t *testing.T) {
|
||||
wd := newWorld()
|
||||
ss := []account.Session{{Device: "Laptop", Seen: wd.clk.t}, {Device: "Phone", Seen: wd.clk.t}, {Device: "Tablet", Seen: wd.clk.t}}
|
||||
gone := ""
|
||||
tt := view(600, 400, wd, func(c *ui.Context) {
|
||||
if r := account.SessionList(c, "Sessions", ss, account.SessionListOptions{}); r.SignedOut >= 0 {
|
||||
gone = ss[r.SignedOut].Device
|
||||
}
|
||||
})
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
// The laptop leaves; the focus stays on the phone's button.
|
||||
ss = ss[1:]
|
||||
tt.Frame()
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if gone != "Phone" {
|
||||
t.Fatalf("Enter after the laptop left signed out %q", gone)
|
||||
}
|
||||
defer func() {
|
||||
if r := fmt.Sprint(recover()); !strings.HasPrefix(r, "account: SessionList") {
|
||||
t.Fatalf("two phones gave %q", r)
|
||||
}
|
||||
}()
|
||||
twins := []account.Session{{Device: "Phone"}, {Device: "Phone"}}
|
||||
view(600, 400, newWorld(), func(c *ui.Context) { account.SessionList(c, "S", twins, account.SessionListOptions{}) })
|
||||
}
|
||||
+10
-1
@@ -1,6 +1,7 @@
|
||||
package account
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
@@ -34,10 +35,18 @@ type SessionListResult struct {
|
||||
// SessionList lists signed-in devices, each with SIGN OUT.
|
||||
func SessionList(c *ui.Context, text string, sessions []Session, o SessionListOptions) SessionListResult {
|
||||
k := pixel.K(c)
|
||||
// Rows are keyed by device, so focus follows its row.
|
||||
seen := map[string]bool{}
|
||||
for _, s := range sessions {
|
||||
if seen[s.Device] {
|
||||
panic(fmt.Sprintf("account: SessionList has %q twice", s.Device))
|
||||
}
|
||||
seen[s.Device] = true
|
||||
}
|
||||
res := SessionListResult{SignedOut: -1}
|
||||
res.Element = ui.Column(c).Width(k.D(pixel.Or("SessionList Width", o.Width, 240))).Gap(k.D(4)).Role(ui.RoleList).Label(text).Children(func() {
|
||||
for i, s := range sessions {
|
||||
ui.Row(c).FillWidth().Gap(k.D(4)).AlignItems(ui.Center).Role(ui.RoleListItem).Children(func() {
|
||||
ui.Row(c).Key(s.Device).FillWidth().Gap(k.D(4)).AlignItems(ui.Center).Role(ui.RoleListItem).Children(func() {
|
||||
if s.Icon != nil {
|
||||
ui.Box(c).Size(k.D(s.Icon.W), k.D(s.Icon.H)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
k.Canvas(p, r).Sprite(0, 0, s.Icon, 0, pixel.Ink, 1)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package chart_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/chart"
|
||||
)
|
||||
|
||||
func TestSpectrumPeaksStayWhenTheClockStepsBack(t *testing.T) {
|
||||
clk, levels := newClock(), []float64{1}
|
||||
tt := view(200, 100, clk, func(c *ui.Context) { chart.SpectrumBars(c, "EQ", levels, chart.SpectrumBarsOptions{}) })
|
||||
levels[0] = 0
|
||||
clk.t = clk.t.Add(200 * time.Millisecond)
|
||||
tt.Frame()
|
||||
before := bytes.Clone(tt.Image().Pix)
|
||||
clk.t = clk.t.Add(-time.Hour)
|
||||
tt.Frame()
|
||||
// A peak flung an hour high would leave the box.
|
||||
if !bytes.Equal(before, tt.Image().Pix) {
|
||||
t.Fatal("the falling peak moved as the clock stepped back")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpectrumPeaksFallInWholeFrames(t *testing.T) {
|
||||
clk, levels := newClock(), []float64{1}
|
||||
tt := view(200, 400, clk, func(c *ui.Context) {
|
||||
chart.SpectrumBars(c, "Peaks", levels, chart.SpectrumBarsOptions{Segments: 100})
|
||||
})
|
||||
levels[0] = 0
|
||||
tt.Frame()
|
||||
seen := map[string]bool{}
|
||||
for range 7 {
|
||||
seen[string(tt.Image().Pix)] = true
|
||||
clk.t = clk.t.Add(10 * time.Millisecond)
|
||||
tt.Frame()
|
||||
}
|
||||
// S5: 60 ms holds two pictures at most.
|
||||
if len(seen) > 2 {
|
||||
t.Fatalf("%d pictures in 60 ms", len(seen))
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -30,10 +30,12 @@ func Legend(c *ui.Context, text string, series []Series, hidden []bool, o Legend
|
||||
for i, s := range series {
|
||||
shown := !hidden[i]
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element { return ui.ToggleBase(c, &shown).Label(s.Name).Disabled(o.Disabled) })
|
||||
if base.Changed() {
|
||||
// The base turns before it learns it is off.
|
||||
if base.Changed() && !base.IsDisabled() {
|
||||
hidden[i], res.Changed = !shown, true
|
||||
slog.Debug("8bit: legend", "label", text, "series", s.Name, "shown", shown)
|
||||
}
|
||||
shown = !hidden[i]
|
||||
ink := pick(s.Slot, i)
|
||||
shell.Size(k.D(10+pixel.Measure(s.Name)), k.D(pixel.LineHeight)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
|
||||
@@ -37,9 +37,9 @@ func SpectrumBars(c *ui.Context, text string, levels []float64, o SpectrumBarsOp
|
||||
if len(pk.at) != len(levels) {
|
||||
pk.at = append([]float64(nil), levels...)
|
||||
}
|
||||
now := k.Now()
|
||||
// A peak falls a bar a second; reduced, it rests.
|
||||
fall := now.Sub(pk.last).Seconds()
|
||||
// Peaks fall a bar a second, stepped; reduced, they rest.
|
||||
now := k.Now().Truncate(time.Second / 15)
|
||||
fall := max(0, now.Sub(pk.last).Seconds())
|
||||
switch {
|
||||
case pk.last.IsZero() || k.Reduce():
|
||||
fall = 1
|
||||
@@ -53,7 +53,7 @@ func SpectrumBars(c *ui.Context, text string, levels []float64, o SpectrumBarsOp
|
||||
pk.moving = pk.moving || pk.at[i] > l
|
||||
}
|
||||
if pk.moving {
|
||||
k.Tick(c, 20)
|
||||
k.Tick(c, 15)
|
||||
}
|
||||
shown := append([]float64(nil), pk.at...)
|
||||
return e.Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package chart_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/chart"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestLegendKeyInAVeilsFirstFrameHidesNothing(t *testing.T) {
|
||||
for _, veil := range []bool{false, true} {
|
||||
hidden, inert := []bool{false, false}, false
|
||||
tt := view(400, 200, newClock(), func(c *ui.Context) {
|
||||
s := pixel.Current(c)
|
||||
s.Inert = inert
|
||||
pixel.Scope(c, s, func() {
|
||||
chart.Legend(c, "Key", []chart.Series{{Name: "HP"}, {Name: "MP"}}, hidden, chart.LegendOptions{})
|
||||
})
|
||||
})
|
||||
tt.Key(0, ui.KeyTab)
|
||||
inert = veil
|
||||
tt.Key(0, ui.KeySpace)
|
||||
if hidden[0] == veil {
|
||||
t.Errorf("veiled %v, Space left %v", veil, fmt.Sprint(hidden))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,15 +34,17 @@ func CallControls(c *ui.Context, text string, muted, camera *bool, o CallControl
|
||||
}
|
||||
g.Finish(st)
|
||||
})
|
||||
return b.Changed() || b.Clicked()
|
||||
// The bases turn before they learn they are off.
|
||||
return (b.Changed() || b.Clicked()) && !b.IsDisabled()
|
||||
}
|
||||
m, cam := *muted, *camera
|
||||
res.Element = ui.Row(c).Gap(k.D(4)).FocusGroup(ui.Horizontal).Role(ui.RoleToolbar).Label(text).Children(func() {
|
||||
if button(func() *ui.Element { return ui.ToggleBase(c, muted).Label(k.Msg("mute")) }, icon.Mic, pixel.Face, pixel.Ink, *muted) {
|
||||
res.Changed = true
|
||||
if button(func() *ui.Element { return ui.ToggleBase(c, &m).Label(k.Msg("mute")) }, icon.Mic, pixel.Face, pixel.Ink, *muted) {
|
||||
*muted, res.Changed = m, true
|
||||
slog.Debug("8bit: call mute", "label", text, "muted", *muted)
|
||||
}
|
||||
if button(func() *ui.Element { return ui.ToggleBase(c, camera).Label(k.Msg("camera")) }, icon.Camera, pixel.Face, pixel.Ink, !*camera) {
|
||||
res.Changed = true
|
||||
if button(func() *ui.Element { return ui.ToggleBase(c, &cam).Label(k.Msg("camera")) }, icon.Camera, pixel.Face, pixel.Ink, !*camera) {
|
||||
*camera, res.Changed = cam, true
|
||||
slog.Debug("8bit: call camera", "label", text, "on", *camera)
|
||||
}
|
||||
if button(func() *ui.Element { return ui.ButtonBase(c).Label(k.Msg("hang_up")) }, icon.Phone, pixel.Bad, pixel.AccentInk, false) {
|
||||
|
||||
+5
-2
@@ -38,10 +38,13 @@ func Reactions(c *ui.Context, text string, rs []Reaction, o ReactionsOptions) Re
|
||||
panic(fmt.Sprintf("chat: reaction %q counts %d, mine %v", r.Emoji, r.Count, r.Mine))
|
||||
}
|
||||
art := emoji(r.Emoji)
|
||||
// The base turns before it learns it is off.
|
||||
v := r.Mine
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
return ui.ToggleBase(c, &r.Mine).Label(fmt.Sprintf("%s %d", r.Emoji, r.Count)).Disabled(o.Disabled).FocusRing(false)
|
||||
return ui.ToggleBase(c, &v).Label(fmt.Sprintf("%s %d", r.Emoji, r.Count)).Disabled(o.Disabled).FocusRing(false)
|
||||
})
|
||||
if base.Changed() {
|
||||
if v != r.Mine && !base.IsDisabled() {
|
||||
r.Mine = v
|
||||
if r.Mine {
|
||||
r.Count++
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package chat_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/chat"
|
||||
)
|
||||
|
||||
func TestKeysInAVeilsFirstFrameTurnNothing(t *testing.T) {
|
||||
cases := map[string]func(c *ui.Context, v *string){
|
||||
"Reactions": func(c *ui.Context, v *string) {
|
||||
rs := []chat.Reaction{{Emoji: "love", Count: 1}}
|
||||
fmt.Sscan(*v, &rs[0].Count)
|
||||
chat.Reactions(c, "Reactions", rs, chat.ReactionsOptions{})
|
||||
*v = fmt.Sprint(rs[0].Count)
|
||||
},
|
||||
"CallControls": func(c *ui.Context, v *string) {
|
||||
muted := *v == "true"
|
||||
chat.CallControls(c, "Call", &muted, new(bool), chat.CallControlsOptions{})
|
||||
*v = fmt.Sprint(muted)
|
||||
},
|
||||
}
|
||||
for name, build := range cases {
|
||||
for _, veil := range []bool{false, true} {
|
||||
start := map[string]string{"Reactions": "1", "CallControls": "false"}[name]
|
||||
v, wd := start, &world{clk: newClock()}
|
||||
tt := view(400, 200, wd, func(c *ui.Context) { build(c, &v) })
|
||||
tt.Key(0, ui.KeyTab)
|
||||
wd.inert = veil
|
||||
tt.Key(0, ui.KeySpace)
|
||||
if (v != start) == veil {
|
||||
t.Errorf("%s: veiled %v, Space made %s", name, veil, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposerSendsNothingInAVeilsFirstFrame(t *testing.T) {
|
||||
for _, veil := range []bool{false, true} {
|
||||
draft, sent, wd := "", "", &world{clk: newClock()}
|
||||
tt := view(400, 200, wd, func(c *ui.Context) {
|
||||
if r := chat.Composer(c, "Say", &draft, chat.ComposerOptions{}); r.Sent != "" {
|
||||
sent = r.Sent
|
||||
}
|
||||
})
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Type("hi")
|
||||
wd.inert = veil
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if (sent == "hi") == veil {
|
||||
t.Errorf("veiled %v, Enter sent %q", veil, sent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package code_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/code"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestSpriteSheetHoldsAFrameWhenTheClockStepsBack(t *testing.T) {
|
||||
wd, frame, playing := newWorld(), 0, true
|
||||
sp := pixel.MustSprite("#\n\na")
|
||||
tt := view(300, 200, wd, func(c *ui.Context) { code.SpriteSheet(c, "Frames", sp, &frame, &playing, code.SpriteSheetOptions{}) })
|
||||
wd.clk.t = wd.clk.t.Add(-200 * time.Millisecond)
|
||||
tt.Frame()
|
||||
tt.Frame()
|
||||
if frame < 0 {
|
||||
t.Fatalf("frame %d", frame)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -38,7 +38,7 @@ func CodeBlock(c *ui.Context, text, src string, o CodeBlockOptions) CodeBlockRes
|
||||
at := ui.Local(e, copiedKey{}, func() time.Time { return time.Time{} })
|
||||
// The button says COPIED! for two seconds.
|
||||
label := k.Msg("copy")
|
||||
if k.Since(c, *at, 1, 2) < 2 {
|
||||
if k.Since(c, *at, 4, 8) < 8 {
|
||||
label = k.Msg("copied")
|
||||
}
|
||||
e.Children(func() {
|
||||
|
||||
+2
-2
@@ -35,10 +35,10 @@ func GlyphMap(c *ui.Context, text string, at *rune, o GlyphMapOptions) GlyphMapR
|
||||
*at = page + rune(cur)
|
||||
// PageUp and PageDown turn a page, keeping the column.
|
||||
if !base.IsDisabled() {
|
||||
if base.Shortcut(0, ui.KeyPageUp) && *at >= 128 {
|
||||
if pixel.Shortcut(base, 0, ui.KeyPageUp) && *at >= 128 {
|
||||
*at -= 128
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyPageDown) && *at+128 <= unicode.MaxRune {
|
||||
if pixel.Shortcut(base, 0, ui.KeyPageDown) && *at+128 <= unicode.MaxRune {
|
||||
*at += 128
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ func SpriteSheet(c *ui.Context, text string, sp *pixel.Sprite, frame *int, playi
|
||||
}
|
||||
if *playing {
|
||||
per := time.Second / time.Duration(fps)
|
||||
m := int(k.Now().Sub(sh.since) / per)
|
||||
m := int(k.Elapsed(&sh.since) / per)
|
||||
*frame = (sh.from + m) % n
|
||||
k.At(c, sh.since.Add(time.Duration(m+1)*per))
|
||||
}
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ func Terminal(c *ui.Context, text string, lines []string, input *string, o Termi
|
||||
}
|
||||
})
|
||||
// Up and Down are history here, taken from the editor.
|
||||
if up, down := row.Shortcut(0, ui.KeyUp), row.Shortcut(0, ui.KeyDown); (up || down) && !off {
|
||||
if up, down := pixel.Shortcut(row, 0, ui.KeyUp), pixel.Shortcut(row, 0, ui.KeyDown); (up || down) && !off {
|
||||
res.Changed = recall(tm, input, up) || res.Changed
|
||||
}
|
||||
})
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ func Carousel(c *ui.Context, text string, pages int, page *int, o CarouselOption
|
||||
wait := every == 0 || o.Disabled || pixel.Current(c).Inert || k.Reduce() || col.Hovered() || col.FocusWithin()
|
||||
if wait {
|
||||
*since = k.Now()
|
||||
} else if k.Since(c, *since, 1, every) == every {
|
||||
} else if k.Since(c, *since, 4, 4*every) == 4*every {
|
||||
turn(1)
|
||||
}
|
||||
col.Children(func() {
|
||||
|
||||
+6
-2
@@ -23,10 +23,14 @@ func Collapsible(c *ui.Context, title string, open *bool, o CollapsibleOptions,
|
||||
col := ui.Column(c).AlignItems(ui.Stretch)
|
||||
res := CollapsibleResult{Element: col}
|
||||
col.Children(func() {
|
||||
// The base turns before it learns it is off.
|
||||
v := *open
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
return ui.CollapsibleBase(c, open).Trigger.Label(title).Disabled(o.Disabled)
|
||||
return ui.CollapsibleBase(c, &v).Trigger.Label(title).Disabled(o.Disabled)
|
||||
})
|
||||
res.Changed = base.Changed()
|
||||
if res.Changed = v != *open && !base.IsDisabled(); res.Changed {
|
||||
*open = v
|
||||
}
|
||||
shown := *open
|
||||
shell.Height(k.D(pixel.LineHeight + 6)).MinWidth(k.D(pixel.Measure(title) + 20)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
|
||||
+3
-2
@@ -86,7 +86,8 @@ func GridView(c *ui.Context, text string, n int, selected *int, o GridViewOption
|
||||
{ui.KeyLeft, old - 1}, {ui.KeyRight, old + 1}, {ui.KeyUp, old - cols}, {ui.KeyDown, old + cols},
|
||||
{ui.KeyHome, 0}, {ui.KeyEnd, n - 1},
|
||||
} {
|
||||
if box.Shortcut(0, m.key) && m.to >= 0 && m.to < n {
|
||||
// The grid stays enabled, so off is checked here.
|
||||
if !off && pixel.Shortcut(box, 0, m.key) && m.to >= 0 && m.to < n {
|
||||
*selected = m.to
|
||||
}
|
||||
}
|
||||
@@ -94,7 +95,7 @@ func GridView(c *ui.Context, text string, n int, selected *int, o GridViewOption
|
||||
res.Changed = true
|
||||
s.rows.ScrollIntoView(*selected / cols)
|
||||
}
|
||||
if box.Shortcut(0, ui.KeyEnter) {
|
||||
if !off && pixel.Shortcut(box, 0, ui.KeyEnter) {
|
||||
res.Submitted = true
|
||||
}
|
||||
if off {
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ func Outline[K comparable](c *ui.Context, text string, roots []K, children func(
|
||||
pixel.Text(c, label(r.key), pixel.TextOptions{Slot: ink})
|
||||
})
|
||||
})).Height(k.D(pixel.Or("Outline Height", o.Height, 140))).Label(text)
|
||||
right, left := e.Shortcut(0, ui.KeyRight), e.Shortcut(0, ui.KeyLeft)
|
||||
right, left := pixel.Shortcut(e, 0, ui.KeyRight), pixel.Shortcut(e, 0, ui.KeyLeft)
|
||||
if (right || left) && st.at >= 0 && !off {
|
||||
at, toggled := steer(rows, st.at, st.opened, right)
|
||||
if toggled {
|
||||
|
||||
+4
-4
@@ -50,16 +50,16 @@ func Sortable(c *ui.Context, text string, items *[]string, o SortableOptions) So
|
||||
*items = slices.Insert(slices.Delete(*items, from, from+1), to, it)
|
||||
s.cur, res.Changed = to, true
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyDown) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyDown) {
|
||||
s.cur = min(s.cur+1, n-1)
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyUp) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyUp) {
|
||||
s.cur = max(s.cur-1, 0)
|
||||
}
|
||||
if base.Shortcut(ui.Alt, ui.KeyDown) {
|
||||
if pixel.Shortcut(base, ui.Alt, ui.KeyDown) {
|
||||
move(s.cur, s.cur+1)
|
||||
}
|
||||
if base.Shortcut(ui.Alt, ui.KeyUp) {
|
||||
if pixel.Shortcut(base, ui.Alt, ui.KeyUp) {
|
||||
move(s.cur, s.cur-1)
|
||||
}
|
||||
// A press takes a row; dragging carries it.
|
||||
|
||||
+11
-8
@@ -71,7 +71,8 @@ func Tree(c *ui.Context, text string, roots []TreeNode, selected *string, o Tree
|
||||
if len(rows) == 0 {
|
||||
return TreeResult{Element: shell}
|
||||
}
|
||||
at := 0
|
||||
// An unknown choice stays, unlit, until a key or click.
|
||||
at := -1
|
||||
for i, r := range rows {
|
||||
if r.key == *selected {
|
||||
at = i
|
||||
@@ -79,23 +80,23 @@ func Tree(c *ui.Context, text string, roots []TreeNode, selected *string, o Tree
|
||||
}
|
||||
old := *selected
|
||||
res := TreeResult{Element: shell}
|
||||
if base.Shortcut(0, ui.KeyDown) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyDown) {
|
||||
at = min(at+1, len(rows)-1)
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyUp) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyUp) {
|
||||
at = max(at-1, 0)
|
||||
}
|
||||
right, left := base.Shortcut(0, ui.KeyRight), base.Shortcut(0, ui.KeyLeft)
|
||||
if right || left {
|
||||
right, left := pixel.Shortcut(base, 0, ui.KeyRight), pixel.Shortcut(base, 0, ui.KeyLeft)
|
||||
if (right || left) && at >= 0 {
|
||||
at, res.Toggled = steer(rows, at, *opened, right)
|
||||
}
|
||||
enter, space := base.Shortcut(0, ui.KeyEnter), base.Shortcut(0, ui.KeySpace)
|
||||
enter, space := pixel.Shortcut(base, 0, ui.KeyEnter), pixel.Shortcut(base, 0, ui.KeySpace)
|
||||
toggle := func(i int) {
|
||||
if key := rows[i].key; rows[i].branch {
|
||||
(*opened)[key], res.Toggled = !(*opened)[key], true
|
||||
}
|
||||
}
|
||||
if enter || space {
|
||||
if (enter || space) && at >= 0 {
|
||||
toggle(at)
|
||||
}
|
||||
// Enter and Space are taken, so clicks come from pointers.
|
||||
@@ -112,7 +113,9 @@ func Tree(c *ui.Context, text string, roots []TreeNode, selected *string, o Tree
|
||||
rows = flatten(tops, children, *opened)
|
||||
at = min(at, len(rows)-1)
|
||||
}
|
||||
*selected = rows[at].key
|
||||
if at >= 0 {
|
||||
*selected = rows[at].key
|
||||
}
|
||||
res.Changed = *selected != old
|
||||
base.Description(strings.TrimSpace(nodes[*selected].Label + " " + nodes[*selected].Value))
|
||||
shown := rows
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package data_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/data"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestCollapsibleKeyInAVeilsFirstFrameOpensNothing(t *testing.T) {
|
||||
for _, veil := range []bool{false, true} {
|
||||
open, inert := false, false
|
||||
tt := underVeil(&inert, func(c *ui.Context) {
|
||||
data.Collapsible(c, "Quests", &open, data.CollapsibleOptions{}, func() { pixel.Text(c, "Slay", pixel.TextOptions{}) })
|
||||
})
|
||||
tt.Key(0, ui.KeyTab)
|
||||
inert = veil
|
||||
tt.Key(0, ui.KeySpace)
|
||||
if open == veil {
|
||||
t.Errorf("veiled %v, Space left it open %v", veil, open)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package data_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/data"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
// underVeil builds fn where the test raises a veil.
|
||||
func underVeil(inert *bool, fn func(c *ui.Context)) *ui.Tester {
|
||||
return view(400, 300, newClock(), false, func(c *ui.Context) {
|
||||
s := pixel.Current(c)
|
||||
s.Inert = *inert
|
||||
pixel.Scope(c, s, func() { fn(c) })
|
||||
})
|
||||
}
|
||||
|
||||
var tree = []data.TreeNode{{Label: "Maps", Children: []data.TreeNode{{Label: "Cave"}}}, {Label: "Saves"}}
|
||||
|
||||
func TestVeilsStopKeysInTheirFirstFrame(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
build func(c *ui.Context, v *string)
|
||||
mods ui.Modifiers
|
||||
key ui.Key
|
||||
}{
|
||||
"Sortable": {func(c *ui.Context, v *string) {
|
||||
items := []string{"A", "B", "C"}
|
||||
if r := data.Sortable(c, "List", &items, data.SortableOptions{}); r.Changed {
|
||||
*v = fmt.Sprint(items)
|
||||
}
|
||||
}, ui.Alt, ui.KeyDown},
|
||||
"Tree": {func(c *ui.Context, v *string) { data.Tree(c, "Files", tree, v, data.TreeOptions{}) }, 0, ui.KeyDown},
|
||||
"Inventory": {func(c *ui.Context, v *string) {
|
||||
at := 0
|
||||
fmt.Sscan(*v, &at)
|
||||
bag := []data.Item{{Sprite: icon.Sword, Name: "Sword"}, {Sprite: icon.Gem, Name: "Gem"}}
|
||||
data.Inventory(c, "Bag", bag, &at, data.InventoryOptions{})
|
||||
*v = fmt.Sprint(at)
|
||||
}, 0, ui.KeyRight},
|
||||
} {
|
||||
v, inert := "0", false
|
||||
tt := underVeil(&inert, func(c *ui.Context) { tc.build(c, &v) })
|
||||
tt.Key(0, ui.KeyTab)
|
||||
inert = true
|
||||
tt.Key(tc.mods, tc.key)
|
||||
if v != "0" {
|
||||
t.Errorf("%s: a key in the veil's first frame made %s", name, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDragsStopUnderAVeil(t *testing.T) {
|
||||
items, inert := []string{"A", "B", "C"}, false
|
||||
tt := underVeil(&inert, func(c *ui.Context) { data.Sortable(c, "List", &items, data.SortableOptions{}) })
|
||||
r := find(t, tt, "List")
|
||||
row := r.H / 3
|
||||
tt.Press(r.X+10, r.Y+row/2)
|
||||
tt.Move(r.X+10, r.Y+row*3/2)
|
||||
moved := fmt.Sprint(items)
|
||||
inert = true
|
||||
tt.Move(r.X+10, r.Y+row*5/2)
|
||||
tt.Release(r.X+10, r.Y+row*5/2)
|
||||
if moved != "[B A C]" || fmt.Sprint(items) != moved {
|
||||
t.Errorf("Sortable: %s before the veil, %v under it", moved, items)
|
||||
}
|
||||
bag, at := []data.Item{{Sprite: icon.Sword, Name: "Sword"}, {Sprite: icon.Gem, Name: "Gem"}}, 0
|
||||
inert = false
|
||||
tt = underVeil(&inert, func(c *ui.Context) { data.Inventory(c, "Bag", bag, &at, data.InventoryOptions{}) })
|
||||
r = find(t, tt, "Bag")
|
||||
tt.Press(r.X+20, r.Y+20)
|
||||
tt.Move(r.X+64, r.Y+20)
|
||||
inert = true
|
||||
tt.Release(r.X+64, r.Y+20)
|
||||
if bag[0].Name != "Sword" {
|
||||
t.Error("Inventory swapped under the veil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeLeavesAnUnknownChoiceAlone(t *testing.T) {
|
||||
chosen, changed := "9/9", 0
|
||||
tt := view(400, 300, newClock(), false, func(c *ui.Context) {
|
||||
if data.Tree(c, "Files", tree, &chosen, data.TreeOptions{}).Changed {
|
||||
changed++
|
||||
}
|
||||
})
|
||||
if chosen != "9/9" || changed != 0 {
|
||||
t.Fatalf("an unknown choice became %q, %d changes", chosen, changed)
|
||||
}
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyDown)
|
||||
if chosen != "0" || changed != 1 {
|
||||
t.Fatalf("Down chose %q, %d changes", chosen, changed)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package datetime
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
@@ -17,8 +18,9 @@ type AnalogClockOptions struct {
|
||||
func AnalogClock(c *ui.Context, o AnalogClockOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
size := pixel.Or("AnalogClock Size", o.Size, 48)
|
||||
k.Tick(c, 1)
|
||||
// Wake on the next whole second.
|
||||
now := k.Now()
|
||||
k.At(c, now.Truncate(time.Second).Add(time.Second))
|
||||
h, m, s := now.Clock()
|
||||
// hand ends at turn (0–1 from twelve) and length r.
|
||||
hand := func(cx, cy int, turn float64, r int) (int, int) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package datetime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
@@ -21,13 +22,14 @@ func DigitalClock(c *ui.Context, o DigitalClockOptions) *ui.Element {
|
||||
if sc > 4 {
|
||||
panic(fmt.Sprintf("datetime: DigitalClock Scale %d outside 1–4", sc))
|
||||
}
|
||||
k.Tick(c, 1)
|
||||
// Wake on the next whole second.
|
||||
now := k.Now()
|
||||
k.At(c, now.Truncate(time.Second).Add(time.Second))
|
||||
colon := func() {
|
||||
ui.Box(c).Size(k.D(3*sc), k.D(11*sc)).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
// The colon blinks each half second, unless reduced.
|
||||
if k.Reduce() || g.Step(2)%2 == 0 {
|
||||
if k.Reduce() || g.Step(4)%4 < 2 {
|
||||
g.Rect(sc, 3*sc, sc, sc, pixel.Bad)
|
||||
g.Rect(sc, 7*sc, sc, sc, pixel.Bad)
|
||||
}
|
||||
|
||||
+2
-2
@@ -71,11 +71,11 @@ func month(c *ui.Context, text string, cur *time.Time, disabled, focus bool, mar
|
||||
{ui.KeyPageUp, addMonths(*cur, -1)}, {ui.KeyPageDown, addMonths(*cur, 1)},
|
||||
{ui.KeyHome, cur.AddDate(0, 0, 1-d)}, {ui.KeyEnd, addMonths(cur.AddDate(0, 0, 1-d), 1).AddDate(0, 0, -1)},
|
||||
} {
|
||||
if base.Shortcut(0, mv.key) {
|
||||
if pixel.Shortcut(base, 0, mv.key) {
|
||||
*cur = mv.to
|
||||
}
|
||||
}
|
||||
enter, space := base.Shortcut(0, ui.KeyEnter), base.Shortcut(0, ui.KeySpace)
|
||||
enter, space := pixel.Shortcut(base, 0, ui.KeyEnter), pixel.Shortcut(base, 0, ui.KeySpace)
|
||||
picked = enter || space
|
||||
first := time.Date(y, m, 1, 0, 0, 0, 0, cur.Location())
|
||||
first = first.AddDate(0, 0, -int(first.Weekday()))
|
||||
|
||||
@@ -35,15 +35,15 @@ func SeasonWheel(c *ui.Context, text string, season *pixel.Season, o SeasonWheel
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
return ui.Box(c).Focusable().Role(ui.RoleRadioGroup).Label(text).Disabled(o.Disabled)
|
||||
})
|
||||
if base.Shortcut(0, ui.KeyRight) || base.Shortcut(0, ui.KeyDown) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyRight) || pixel.Shortcut(base, 0, ui.KeyDown) {
|
||||
*season = (*season + 1) % 4
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyLeft) || base.Shortcut(0, ui.KeyUp) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyLeft) || pixel.Shortcut(base, 0, ui.KeyUp) {
|
||||
*season = (*season + 3) % 4
|
||||
}
|
||||
// Enter and Space are taken, so clicks come from pointers.
|
||||
base.Shortcut(0, ui.KeyEnter)
|
||||
base.Shortcut(0, ui.KeySpace)
|
||||
pixel.Shortcut(base, 0, ui.KeyEnter)
|
||||
pixel.Shortcut(base, 0, ui.KeySpace)
|
||||
if base.Clicked() {
|
||||
x, y, _ := base.PointerPosition()
|
||||
*season = pixel.Season(quarter(int(x/k.Dot())-wheel/2, int(y/k.Dot())-wheel/2))
|
||||
|
||||
@@ -43,7 +43,7 @@ func Stopwatch(c *ui.Context, text string, o StopwatchOptions) StopwatchResult {
|
||||
running := !w.since.IsZero()
|
||||
elapsed := w.banked
|
||||
if running {
|
||||
elapsed += now.Sub(w.since)
|
||||
elapsed += k.Elapsed(&w.since)
|
||||
k.Tick(c, 10)
|
||||
}
|
||||
res := StopwatchResult{Element: col}
|
||||
|
||||
+2
-2
@@ -33,10 +33,10 @@ func SunDial(c *ui.Context, text string, hour *int, o SunDialOptions) SunDialRes
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
return ui.Box(c).Focusable().Role(ui.RoleSlider).Label(text).Disabled(o.Disabled)
|
||||
})
|
||||
if base.Shortcut(0, ui.KeyLeft) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyLeft) {
|
||||
*hour = (*hour + 23) % 24
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyRight) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyRight) {
|
||||
*hour = (*hour + 1) % 24
|
||||
}
|
||||
// The width spans the whole day, midnight to midnight.
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package datetime_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/datetime"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestKeysDoNotClickWhereThePointerRests(t *testing.T) {
|
||||
day := time.Date(2026, 10, 12, 9, 30, 0, 0, time.UTC)
|
||||
for name, tc := range map[string]struct {
|
||||
build func(c *ui.Context, v *string)
|
||||
// fx is where, across the control, the pointer rests.
|
||||
fx float32
|
||||
after ui.Key
|
||||
}{
|
||||
"WeekStrip": {func(c *ui.Context, v *string) {
|
||||
d := day
|
||||
datetime.WeekStrip(c, "Week", &d, datetime.WeekStripOptions{})
|
||||
if !d.Equal(day) {
|
||||
*v = d.Format("Jan 2")
|
||||
}
|
||||
}, 0.95, 0},
|
||||
"SeasonWheel": {func(c *ui.Context, v *string) {
|
||||
s := pixel.Spring
|
||||
datetime.SeasonWheel(c, "Week", &s, datetime.SeasonWheelOptions{})
|
||||
if s != pixel.Spring {
|
||||
*v = "turned"
|
||||
}
|
||||
}, 0.9, 0},
|
||||
"TimeInput": {func(c *ui.Context, v *string) {
|
||||
tm := day
|
||||
datetime.TimeInput(c, "Week", &tm, datetime.TimeInputOptions{})
|
||||
// Up after the keys must step the hours.
|
||||
if tm.Minute() != day.Minute() {
|
||||
*v = tm.Format("15:04")
|
||||
}
|
||||
}, 0.9, ui.KeyUp},
|
||||
} {
|
||||
got := ""
|
||||
tt := view(400, 300, newClock(), false, func(c *ui.Context) { tc.build(c, &got) })
|
||||
r := find(t, tt, "Week")
|
||||
tt.Move(r.X+r.W*tc.fx, r.Y+r.H/2)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeySpace)
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if tc.after != 0 {
|
||||
tt.Key(0, tc.after)
|
||||
}
|
||||
if got != "" {
|
||||
t.Errorf("%s: Space and Enter clicked where the pointer rests: %s", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopwatchHoldsWhenTheClockStepsBack(t *testing.T) {
|
||||
clk := newClock()
|
||||
tt := view(400, 200, clk, false, func(c *ui.Context) { datetime.Stopwatch(c, "Watch", datetime.StopwatchOptions{}) })
|
||||
if err := tt.Click("START"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clk.t = clk.t.Add(-1200 * time.Millisecond)
|
||||
tt.Frame()
|
||||
if err := tt.Click("STOP"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(strings.Join(tt.Texts(), " "), "-") {
|
||||
t.Fatalf("a negative time: %q", tt.Texts())
|
||||
}
|
||||
}
|
||||
@@ -47,22 +47,22 @@ func TimeInput(c *ui.Context, text string, tm *time.Time, o TimeInputOptions) Ti
|
||||
}
|
||||
s.first = -1
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyUp) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyUp) {
|
||||
step(1)
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyDown) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyDown) {
|
||||
step(-1)
|
||||
}
|
||||
for _, mv := range []struct {
|
||||
key ui.Key
|
||||
seg int
|
||||
}{{ui.KeyLeft, 0}, {ui.KeyRight, 1}} {
|
||||
if base.Shortcut(0, mv.key) {
|
||||
if pixel.Shortcut(base, 0, mv.key) {
|
||||
s.seg, s.first = mv.seg, -1
|
||||
}
|
||||
}
|
||||
for d := range 10 {
|
||||
if !base.Shortcut(0, ui.Key0+ui.Key(d)) {
|
||||
if !pixel.Shortcut(base, 0, ui.Key0+ui.Key(d)) {
|
||||
continue
|
||||
}
|
||||
top := 23
|
||||
@@ -86,8 +86,8 @@ func TimeInput(c *ui.Context, text string, tm *time.Time, o TimeInputOptions) Ti
|
||||
}
|
||||
}
|
||||
// Enter and Space are taken, so clicks come from pointers.
|
||||
base.Shortcut(0, ui.KeyEnter)
|
||||
base.Shortcut(0, ui.KeySpace)
|
||||
pixel.Shortcut(base, 0, ui.KeyEnter)
|
||||
pixel.Shortcut(base, 0, ui.KeySpace)
|
||||
if base.Clicked() {
|
||||
x, _, _ := base.PointerPosition()
|
||||
s.seg, s.first = 0, -1
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ func Timer(c *ui.Context, text string, set *time.Duration, o TimerOptions) Timer
|
||||
}
|
||||
}
|
||||
// It flashes for three seconds after ringing.
|
||||
flash := !t.rang.IsZero() && now.Sub(t.rang) < 3*time.Second
|
||||
flash := !t.rang.IsZero() && k.Elapsed(&t.rang) < 3*time.Second
|
||||
if flash {
|
||||
k.Tick(c, 4)
|
||||
}
|
||||
|
||||
@@ -29,16 +29,16 @@ func WeekStrip(c *ui.Context, text string, date *time.Time, o WeekStripOptions)
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
return ui.Box(c).Focusable().Role(ui.RoleGroup).Label(text).Disabled(o.Disabled)
|
||||
})
|
||||
if base.Shortcut(0, ui.KeyLeft) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyLeft) {
|
||||
*date = date.AddDate(0, 0, -1)
|
||||
}
|
||||
if base.Shortcut(0, ui.KeyRight) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyRight) {
|
||||
*date = date.AddDate(0, 0, 1)
|
||||
}
|
||||
first := date.AddDate(0, 0, -int(date.Weekday()))
|
||||
// Enter and Space are taken, so clicks come from pointers.
|
||||
base.Shortcut(0, ui.KeyEnter)
|
||||
base.Shortcut(0, ui.KeySpace)
|
||||
pixel.Shortcut(base, 0, ui.KeyEnter)
|
||||
pixel.Shortcut(base, 0, ui.KeySpace)
|
||||
if base.Clicked() {
|
||||
x, _, _ := base.PointerPosition()
|
||||
if i := int(x / k.D(dayW)); x >= 0 && i < 7 {
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ func AppWindow(c *ui.Context, title string, x, y *int, o AppWindowOptions, conte
|
||||
key ui.Key
|
||||
dx, dy int
|
||||
}{{ui.KeyLeft, -4, 0}, {ui.KeyRight, 4, 0}, {ui.KeyUp, 0, -4}, {ui.KeyDown, 0, 4}} {
|
||||
if base.Shortcut(0, m.key) {
|
||||
if pixel.Shortcut(base, 0, m.key) {
|
||||
*x, *y = *x+m.dx, *y+m.dy
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -54,12 +54,12 @@ func Calculator(c *ui.Context, text string, o CalculatorOptions) CalculatorResul
|
||||
// Digits and signs typed on the keyboard press their keys.
|
||||
if picked < 0 && !base.IsDisabled() && !pixel.Typing(c) {
|
||||
for i := range 10 {
|
||||
if base.Shortcut(0, ui.Key0+ui.Key(i)) {
|
||||
if pixel.Shortcut(base, 0, ui.Key0+ui.Key(i)) {
|
||||
picked = indexOf(strconv.Itoa(i))
|
||||
}
|
||||
}
|
||||
for _, s := range signs {
|
||||
if base.Shortcut(s.mods, s.key) {
|
||||
if pixel.Shortcut(base, s.mods, s.key) {
|
||||
picked = indexOf(s.sign)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ func DesktopIcon(c *ui.Context, label string, icon *pixel.Sprite, o DesktopIconO
|
||||
})
|
||||
res := DesktopIconResult{Element: shell}
|
||||
if !base.IsDisabled() {
|
||||
res.Opened = base.Shortcut(0, ui.KeyEnter) || base.DoubleClicked()
|
||||
res.Opened = pixel.Shortcut(base, 0, ui.KeyEnter) || base.DoubleClicked()
|
||||
res.Clicked = base.Clicked()
|
||||
}
|
||||
if res.Clicked || res.Opened {
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ func Minesweeper(c *ui.Context, label string, o MinesweeperOptions) MinesweeperR
|
||||
})
|
||||
shell, base, st, picked := pixel.Pad(c, k.Msg("minefield"), cols, cols*rows, mcell, mcell, &f.cur, o.Disabled)
|
||||
if !base.IsDisabled() && !f.over {
|
||||
if !pixel.Typing(c) && base.Shortcut(0, ui.KeyF) {
|
||||
if !pixel.Typing(c) && pixel.Shortcut(base, 0, ui.KeyF) {
|
||||
res.Flagged = f.toggle(f.cur)
|
||||
}
|
||||
if base.RightClicked() {
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ func RunDialog(c *ui.Context, text string, value *string, o RunDialogOptions) Ru
|
||||
})
|
||||
})
|
||||
// Escape quits from anywhere inside, as CANCEL does.
|
||||
if !o.Disabled && !pixel.Current(c).Inert && win.Shortcut(0, ui.KeyEscape) {
|
||||
if !o.Disabled && !pixel.Current(c).Inert && pixel.Shortcut(win, 0, ui.KeyEscape) {
|
||||
res.Canceled = true
|
||||
}
|
||||
res.Element = win
|
||||
|
||||
+2
-2
@@ -61,13 +61,13 @@ func Snake(c *ui.Context, text string, o SnakeOptions) SnakeResult {
|
||||
d [2]int
|
||||
}{{ui.KeyUp, [2]int{0, -1}}, {ui.KeyDown, [2]int{0, 1}}, {ui.KeyLeft, [2]int{-1, 0}}, {ui.KeyRight, [2]int{1, 0}}} {
|
||||
// A turn back onto its own neck is ignored.
|
||||
if base.Shortcut(0, m.key) && m.d != [2]int{-s.dir[0], -s.dir[1]} && !s.dead {
|
||||
if pixel.Shortcut(base, 0, m.key) && m.d != [2]int{-s.dir[0], -s.dir[1]} && !s.dead {
|
||||
res.Started = res.Started || !s.playing
|
||||
s.next, s.playing = m.d, true
|
||||
}
|
||||
}
|
||||
// Enter, Space or a click starts it, or CONTINUEs.
|
||||
enter, space := base.Shortcut(0, ui.KeyEnter), base.Shortcut(0, ui.KeySpace)
|
||||
enter, space := pixel.Shortcut(base, 0, ui.KeyEnter), pixel.Shortcut(base, 0, ui.KeySpace)
|
||||
if enter || space || base.Clicked() {
|
||||
if s.dead {
|
||||
s.reset(cols, rows, o.Seed, uint64(k.Now().UnixNano()))
|
||||
|
||||
+2
-5
@@ -48,11 +48,8 @@ func StartMenu(c *ui.Context, text string, programs []Program, o StartMenuOption
|
||||
off := o.Disabled || pixel.Current(c).Inert
|
||||
b := input.Button(c, k.Msg("start"), input.ButtonOptions{Icon: icon.Gamepad, Label: text, Disabled: o.Disabled})
|
||||
m := ui.Local(b.Element, startKey{}, func() start { return start{} })
|
||||
// START keeps the focus and parks it while off.
|
||||
if off {
|
||||
// Closing on disable must not return focus to it.
|
||||
if m.open {
|
||||
c.Root().Focus()
|
||||
}
|
||||
m.open = false
|
||||
}
|
||||
res := StartMenuResult{Element: b.Element}
|
||||
@@ -60,7 +57,7 @@ func StartMenu(c *ui.Context, text string, programs []Program, o StartMenuOption
|
||||
switch {
|
||||
case b.Clicked && m.open:
|
||||
m.open = false
|
||||
case b.Clicked, !m.open && !off && b.Element.Shortcut(0, ui.KeyUp):
|
||||
case b.Clicked, !m.open && !off && pixel.Shortcut(b.Element, 0, ui.KeyUp):
|
||||
m.open, m.path = true, []int{0}
|
||||
}
|
||||
if m.open {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package desk_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/desk"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/icon"
|
||||
)
|
||||
|
||||
func TestStartMenuGetsFocusBackAfterAVeil(t *testing.T) {
|
||||
wd := newWorld()
|
||||
programs := []desk.Program{{Name: "Paint", Icon: icon.File}, {Name: "Notes", Icon: icon.File}}
|
||||
tt := view(400, 300, wd, func(c *ui.Context) { desk.StartMenu(c, "Start", programs, desk.StartMenuOptions{}) })
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyUp)
|
||||
if !tt.HasText("Notes") {
|
||||
t.Fatalf("the menu did not open: %q", tt.Texts())
|
||||
}
|
||||
wd.inert = true
|
||||
tt.Frame()
|
||||
wd.inert = false
|
||||
tt.Frame()
|
||||
tt.Key(0, ui.KeyUp)
|
||||
if !tt.HasText("Notes") {
|
||||
t.Fatal("START did not get its focus back after the veil")
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ func TestSpritePlaysAndMirrors(t *testing.T) {
|
||||
sp := pixel.MustSprite("r.\n..\n\n.r\n..")
|
||||
tt := view(100, 100, e, func(c *ui.Context) {
|
||||
ui.Column(c).Children(func() {
|
||||
display.Sprite(c, sp, display.SpriteOptions{FPS: 1, Label: "s"})
|
||||
display.Sprite(c, sp, display.SpriteOptions{FPS: 4, Label: "s"})
|
||||
display.Sprite(c, sp, display.SpriteOptions{Mirror: true, Label: "m"})
|
||||
})
|
||||
})
|
||||
@@ -101,7 +101,7 @@ func TestSpritePlaysAndMirrors(t *testing.T) {
|
||||
if f0[0] != pixel.Bad || at("m")[1] != pixel.Bad {
|
||||
t.Fatalf("frame 0 %v, mirrored %v", f0, at("m"))
|
||||
}
|
||||
e.clk.t = e.clk.t.Add(time.Second)
|
||||
e.clk.t = e.clk.t.Add(time.Second / 4)
|
||||
tt.Frame()
|
||||
if at("s")[1] != pixel.Bad {
|
||||
t.Fatal("the sprite did not play its second frame")
|
||||
|
||||
@@ -41,7 +41,7 @@ func RarityFrame(c *ui.Context, r Rarity, o RarityFrameOptions, children func())
|
||||
return
|
||||
}
|
||||
per := 2*(g.W+g.H) - 4
|
||||
at := g.Step(20) * 2 % per
|
||||
at := g.Step(10) * 4 % per
|
||||
for i := range 6 {
|
||||
x, y := around(g.W, g.H, (at+i)%per)
|
||||
g.Dot(x, y, pixel.Lit)
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import (
|
||||
type SpriteOptions struct {
|
||||
Slot pixel.Slot
|
||||
Scale int
|
||||
// FPS plays the frames; zero shows the first.
|
||||
// FPS plays the frames at 4–15; zero shows the first.
|
||||
FPS int
|
||||
Mirror bool
|
||||
Label string
|
||||
|
||||
@@ -40,7 +40,7 @@ func StatusDot(c *ui.Context, s Status, o StatusDotOptions) *ui.Element {
|
||||
}
|
||||
return art(c, k, label, 7, 7, func(g pixel.Canvas) {
|
||||
g.Disc(3, 3, 3, pixel.Edge)
|
||||
if !(o.Live && s == Online && g.Step(2)%2 == 1) {
|
||||
if !(o.Live && s == Online && g.Step(4)%4 >= 2) {
|
||||
g.Disc(3, 3, 2, statusSlots[s])
|
||||
g.Dot(2, 2, pixel.Lit)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package feedback_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/feedback"
|
||||
)
|
||||
|
||||
func TestCountdownsHoldWhenTheClockStepsBack(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
build func(c *ui.Context)
|
||||
start string
|
||||
}{
|
||||
"ContinuePrompt": {func(c *ui.Context) { feedback.ContinuePrompt(c, feedback.ContinuePromptOptions{}) }, "10"},
|
||||
"ErrorState": {func(c *ui.Context) { feedback.ErrorState(c, "LOST", feedback.ErrorStateOptions{}) }, "9"},
|
||||
} {
|
||||
clk := newClock()
|
||||
tt := view(500, 400, clk, false, tc.build)
|
||||
clk.t = clk.t.Add(-time.Hour)
|
||||
tt.Frame()
|
||||
// The count, a text's last word, never passes its start.
|
||||
start, _ := strconv.Atoi(tc.start)
|
||||
for _, s := range tt.Texts() {
|
||||
f := strings.Fields(s)
|
||||
if len(f) == 0 {
|
||||
continue
|
||||
}
|
||||
if n, err := strconv.Atoi(f[len(f)-1]); err == nil && n > start {
|
||||
t.Errorf("%s counts %d after the clock stepped back", name, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToastsExpireAfterTheClockStepsBack(t *testing.T) {
|
||||
clk := newClock()
|
||||
queue := []feedback.Toast{{Message: "SAVED"}}
|
||||
tt := view(500, 300, clk, false, func(c *ui.Context) { feedback.Toasts(c, &queue, feedback.ToastsOptions{}) })
|
||||
clk.t = clk.t.Add(-time.Hour)
|
||||
tt.Frame()
|
||||
clk.t = clk.t.Add(4 * time.Second)
|
||||
tt.Frame()
|
||||
if len(queue) != 0 {
|
||||
t.Fatalf("a toast outlived its three seconds: %d left", len(queue))
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func ContinuePrompt(c *ui.Context, o ContinuePromptOptions) ContinuePromptResult
|
||||
n := pixel.Or("ContinuePrompt Seconds", o.Seconds, 10)
|
||||
col := ui.Column(c).Gap(k.D(6)).AlignItems(ui.Center).Role(ui.RoleGroup).Label(k.Msg("continue"))
|
||||
st := ui.Local(col, promptKey{}, func() prompt { return prompt{start: k.Now()} })
|
||||
left := max(0, n-int(k.Now().Sub(st.start)/time.Second))
|
||||
left := max(0, n-max(0, int(k.Now().Sub(st.start)/time.Second)))
|
||||
res := ContinuePromptResult{Element: col}
|
||||
if left == 0 && !st.over {
|
||||
st.over, res.TimedOut = true, true
|
||||
|
||||
+1
-1
@@ -18,6 +18,6 @@ var shakes = [...]int{2, -2, 2, -2, 1, -1, 0}
|
||||
func Shake(c *ui.Context, o ShakeOptions, content func()) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
e := ui.Box(c)
|
||||
dx := shakes[k.Shot(c, e, o.Cue, 20, len(shakes)-1)]
|
||||
dx := shakes[k.Shot(c, e, o.Cue, 15, len(shakes)-1)]
|
||||
return e.Padding(0, k.D(2-dx), 0, k.D(2+dx)).Children(content)
|
||||
}
|
||||
+2
-2
@@ -71,7 +71,7 @@ func ErrorState(c *ui.Context, message string, o ErrorStateOptions) ErrorStateRe
|
||||
k := pixel.K(c)
|
||||
col := ui.Column(c).Gap(k.D(8)).Padding(k.D(16)).AlignItems(ui.Center).Role(ui.RoleGroup).Label(k.Msg("game_over"))
|
||||
start := ui.Local(col, startKey{}, func() time.Time { return k.Now() })
|
||||
left := max(0, 9-int(k.Now().Sub(*start)/time.Second))
|
||||
left := max(0, 9-max(0, int(k.Now().Sub(*start)/time.Second)))
|
||||
if left > 0 {
|
||||
k.Tick(c, 10)
|
||||
}
|
||||
@@ -103,7 +103,7 @@ func SuccessState(c *ui.Context, message string, o SuccessStateOptions) *ui.Elem
|
||||
// Stars come four a second; Reduce Motion shows them all.
|
||||
shown := n + 1
|
||||
if !k.Reduce() {
|
||||
shown = min(n+1, int(k.Now().Sub(*start)*4/time.Second))
|
||||
shown = min(n+1, max(0, int(k.Now().Sub(*start)*4/time.Second)))
|
||||
if shown <= n {
|
||||
k.Tick(c, 8)
|
||||
}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ func Toasts(c *ui.Context, queue *[]Toast, o ToastsOptions) ToastsResult {
|
||||
if life == 0 {
|
||||
life = 3 * time.Second
|
||||
}
|
||||
if t.shown.IsZero() || now.Sub(t.shown) < life {
|
||||
if t.shown.IsZero() || k.Elapsed(&t.shown) < life {
|
||||
kept = append(kept, t)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ func SharePanel(c *ui.Context, text, link string, access *Access, o SharePanelOp
|
||||
at := ui.Local(e, shareKey{}, func() time.Time { return time.Time{} })
|
||||
// The button says COPIED! for two seconds.
|
||||
label := k.Msg("copy")
|
||||
if k.Since(c, *at, 1, 2) < 2 {
|
||||
if k.Since(c, *at, 4, 8) < 8 {
|
||||
label = k.Msg("copied")
|
||||
}
|
||||
e.Children(func() {
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type PaletteCycleOptions struct {
|
||||
// FPS is steps a second; zero means 8.
|
||||
// FPS is 4–15 steps a second; zero means 8.
|
||||
FPS int
|
||||
// Slot inks the sprite's # dots.
|
||||
Slot pixel.Slot
|
||||
|
||||
+2
-1
@@ -99,7 +99,8 @@ func (a *App) step(d int) {
|
||||
// logger records a demo's events for the log panel.
|
||||
func (a *App) logger(e Entry) func(string) {
|
||||
return func(ev string) {
|
||||
slog.Debug("8bit: demo", "entry", e.Slug(), "event", ev)
|
||||
// The panel shows what was typed; logs never do.
|
||||
slog.Debug("8bit: demo", "entry", e.Slug())
|
||||
l := append(a.logs[e.Slug()], ev)
|
||||
a.logs[e.Slug()] = l[max(0, len(l)-8):]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestChatDemosLogNoTypedText(t *testing.T) {
|
||||
for _, tc := range []struct{ path, field string }{{"/c/323-composer", "Message"}, {"/c/329-mentioninput", "Mention"}} {
|
||||
a, tt := open(tc.path)
|
||||
click(t, tt, tc.field)
|
||||
tt.Type("xyzzy")
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
logs := a.logs[tc.path[3:]]
|
||||
if len(logs) == 0 || strings.Contains(strings.Join(logs, "\n"), "xyzzy") {
|
||||
t.Errorf("%s logged %q", tc.path, logs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaletteInUseSpeaksTheLocale(t *testing.T) {
|
||||
a, tt := open("/")
|
||||
click(t, tt, "language")
|
||||
if a.locale != pixel.Zh {
|
||||
t.Fatal("no Chinese")
|
||||
}
|
||||
name := pixel.Palettes[a.palette].Name + ", 使用中"
|
||||
if _, ok := tt.Find(name); !ok {
|
||||
t.Fatalf("no %q in %q", name, tt.Texts())
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -104,7 +104,7 @@ func (a *App) sidebar(c *ui.Context) {
|
||||
}
|
||||
|
||||
func (a *App) statusBar(c *ui.Context) {
|
||||
layout.StatusBar(c, "/ search · P palette · [ ] prev next", fmt.Sprintf("%d entries", len(Entries())))
|
||||
layout.StatusBar(c, []string{"/ search · P palette · [ ] prev next", fmt.Sprintf("%d entries", len(Entries()))}, layout.StatusBarOptions{})
|
||||
}
|
||||
|
||||
// press is a custom-drawn chrome tile; true when clicked.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestDemosSurviveTheClockSteppingBack(t *testing.T) {
|
||||
negative := regexp.MustCompile(`(^|[\s(:])-\d`)
|
||||
for _, e := range Entries() {
|
||||
at := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC)
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }})
|
||||
ui.Column(c).Fill().Children(func() { e.Demo(c, func(string) {}) })
|
||||
}, 640, 480)
|
||||
// Press up to three buttons, run, then fall back.
|
||||
for _, b := range buttons(tt, 3) {
|
||||
if r, ok := tt.Find(b); ok {
|
||||
tt.ClickAt(r.X+r.W/2, r.Y+r.H/2)
|
||||
}
|
||||
}
|
||||
at = at.Add(500 * time.Millisecond)
|
||||
tt.Frame()
|
||||
// Negative numbers shown before are content, not faults.
|
||||
before := map[string]bool{}
|
||||
for _, s := range tt.Texts() {
|
||||
before[s] = true
|
||||
}
|
||||
for _, d := range []time.Duration{-time.Hour, 300 * time.Millisecond} {
|
||||
at = at.Add(d)
|
||||
tt.Frame()
|
||||
}
|
||||
for _, s := range tt.Texts() {
|
||||
if negative.MatchString(s) && !before[s] {
|
||||
t.Errorf("%s shows %q after the clock stepped back", e.Slug(), s)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buttons names up to n pressable words on screen.
|
||||
func buttons(tt *ui.Tester, n int) []string {
|
||||
var out []string
|
||||
for _, s := range tt.Texts() {
|
||||
if len(out) < n && len(s) > 1 && s == upper(s) && strings.ContainsAny(s, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func upper(s string) string {
|
||||
b := []byte(s)
|
||||
for i, c := range b {
|
||||
if c >= 'a' && c <= 'z' {
|
||||
b[i] = c - 32
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestPlayingMediaSurvivesTheClockSteppingBack(t *testing.T) {
|
||||
for _, slug := range []string{"346-audioplayer", "353-cassette", "360-videoframe"} {
|
||||
e, _ := find(slug)
|
||||
at := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC)
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }})
|
||||
ui.Column(c).Fill().Children(func() { e.Demo(c, func(string) {}) })
|
||||
}, 640, 480)
|
||||
if err := tt.Click("PLAY"); err != nil {
|
||||
t.Fatalf("%s: %v", slug, err)
|
||||
}
|
||||
for _, d := range []time.Duration{500 * time.Millisecond, -time.Second, 200 * time.Millisecond} {
|
||||
at = at.Add(d)
|
||||
tt.Frame()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
func TestDemoEventsKeepTypedTextOutOfLogs(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
|
||||
defer slog.SetDefault(prev)
|
||||
// A wrong vault code: four digits, then WRONG CODE.
|
||||
a, tt := open("/c/094-pininput")
|
||||
click(t, tt, "Vault code")
|
||||
tt.Type("9876")
|
||||
shown := strings.Join(a.logs["094-pininput"], "\n")
|
||||
if !strings.Contains(shown, "WRONG CODE") || !strings.Contains(buf.String(), "8bit: demo") {
|
||||
t.Fatalf("no demo event logged: %q", shown)
|
||||
}
|
||||
if strings.Contains(buf.String(), "9876") || strings.Contains(shown, "9876") {
|
||||
t.Fatalf("the code reached a log: %q", shown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDemoPanelsShowWhatLogsDoNot(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
prev := slog.Default()
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
|
||||
defer slog.SetDefault(prev)
|
||||
a, tt := open("/c/099-editabletext")
|
||||
click(t, tt, "Hero name")
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
tt.Type("Zork")
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
shown := strings.Join(a.logs["099-editabletext"], "\n")
|
||||
if !strings.Contains(shown, "Zork") || strings.Contains(buf.String(), "Zork") {
|
||||
t.Fatalf("panel %q; the log has the name: %v", shown, strings.Contains(buf.String(), "Zork"))
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,8 @@ var chartEntries = []Entry{
|
||||
{225, "Histogram", "直方图", "chart", "Damage rolls in bins. ROLL throws ten more.", demoHistogram},
|
||||
{226, "ScatterPlot", "散点图", "chart", "Every hit by distance and damage, one 2×2 dot each.", demoScatterPlot},
|
||||
{227, "BubbleChart", "气泡图", "chart", "Towns by trade and danger, sized by how many live there.", demoBubbleChart},
|
||||
{228, "Heatmap", "热力图", "chart", "Raids by day and hour. Hot cells dither red.", demoHeatmap},
|
||||
{229, "CalendarHeatmap", "日历热力图", "chart", "A year of play, a square a day. A commit graph for slimes.", demoCalendarHeatmap},
|
||||
{228, "Heatmap", "密度图", "chart", "Raids by day and hour. Hot cells dither red.", demoHeatmap},
|
||||
{229, "CalendarHeatmap", "日历密度图", "chart", "A year of play, a square a day. A commit graph for slimes.", demoCalendarHeatmap},
|
||||
{230, "Gauge", "仪表", "chart", "Push the throttle; the needle follows.", demoGauge},
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package gallery
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
@@ -16,7 +17,7 @@ var chatEntries = []Entry{
|
||||
{323, "Composer", "输入栏", "chat", "Enter sends, Shift+Enter breaks the line. SEND ▶ works too.", demoComposer},
|
||||
{324, "TypingIndicator", "正在输入", "chat", "Three dots hop while someone types. Suspense!", demoTypingIndicator},
|
||||
{325, "ChatList", "会话列表", "chat", "Who said what last, and how much you missed.", demoChatList},
|
||||
{326, "ReadReceipt", "已读回执", "chat", "One check: sent. Two checks: seen. No excuses.", demoReadReceipt},
|
||||
{326, "ReadReceipt", "已读标记", "chat", "One check: sent. Two checks: seen. No excuses.", demoReadReceipt},
|
||||
{327, "Reactions", "表情回应", "chat", "Click a chip to add your face to the pile, again to take it back.", demoReactions},
|
||||
{328, "EmojiPicker", "表情选择", "chat", "Faces, the world and loot, eight to a row.", demoEmojiPicker},
|
||||
{329, "MentionInput", "提及输入", "chat", "Type @ and a few letters; Tab finishes the name.", demoMentionInput},
|
||||
@@ -92,10 +93,11 @@ func demoComposer(c *ui.Context, log func(string)) {
|
||||
s := chatState(c)
|
||||
r := chat.Composer(c, "Message", &s.draft, chat.ComposerOptions{Placeholder: "Say something…"})
|
||||
if r.Changed {
|
||||
log(fmt.Sprintf("draft %q", s.draft))
|
||||
log(fmt.Sprintf("draft of %d letters", utf8.RuneCountInString(s.draft)))
|
||||
}
|
||||
// Counts, not words: logs never hold what was typed.
|
||||
if r.Sent != "" {
|
||||
log("sent: " + r.Sent)
|
||||
log(fmt.Sprintf("sent %d letters", utf8.RuneCountInString(r.Sent)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,13 +151,13 @@ func demoMentionInput(c *ui.Context, log func(string)) {
|
||||
s := chatState(c)
|
||||
r := chat.MentionInput(c, "Mention", &s.mention, []string{"Aria", "Arthur", "Brom", "Mittens"}, chat.MentionInputOptions{Placeholder: "Try @ar"})
|
||||
if r.Changed {
|
||||
log(fmt.Sprintf("text %q", s.mention))
|
||||
log(fmt.Sprintf("text of %d letters", utf8.RuneCountInString(s.mention)))
|
||||
}
|
||||
if r.Mentioned != "" {
|
||||
log("mentioned " + r.Mentioned)
|
||||
}
|
||||
if r.Submitted {
|
||||
log("sent: " + s.mention)
|
||||
log(fmt.Sprintf("sent %d letters", utf8.RuneCountInString(s.mention)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ func demoFPSMeter(c *ui.Context, log func(string)) {
|
||||
}
|
||||
}
|
||||
if !k.Reduce() {
|
||||
if t := k.Tick(c, 12); s.last.IsZero() || k.Now().Sub(s.last) >= time.Second/12 {
|
||||
if t := k.Tick(c, 12); s.last.IsZero() || k.Elapsed(&s.last) >= time.Second/12 {
|
||||
s.last = k.Now()
|
||||
add(t)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ var controlEntries = []Entry{
|
||||
{90, "TokenField", "标记输入", "input", "Tags before the caret. Enter or a comma adds; Backspace on empty takes the last back.", demoTokenField},
|
||||
{91, "Segmented", "分段控件", "input", "Joined segments, one lit. Arrows move the light.", demoSegmented},
|
||||
{92, "Rating", "评分", "input", "Stars or hearts. Click or arrows to rate; click the same mark to clear.", demoRating},
|
||||
{93, "Knob", "旋钮", "input", "A rotary knob that clicks through 16 angles. Drag up or down, or use the arrows.", demoKnob},
|
||||
{93, "Knob", "转钮", "input", "A rotary knob that clicks through 16 angles. Drag up or down, or use the arrows.", demoKnob},
|
||||
{94, "PinInput", "验证码", "input", "One box per digit: typing moves on, Backspace steps back, paste fills them all.", demoPinInput},
|
||||
{95, "NameEntry", "姓名登记", "input", "HIGH SCORE! Three initials from the letter grid, then END.", demoNameEntry},
|
||||
{96, "VirtualKeyboard", "屏幕键盘", "input", "A keyboard on screen, for pads and touch: letters, digits, space, erase, enter.", demoVirtualKeyboard},
|
||||
@@ -29,9 +29,9 @@ var controlEntries = []Entry{
|
||||
{100, "Field", "表单项", "input", "A label above, a hint below, and a red ! when something is wrong.", demoField},
|
||||
{101, "Fieldset", "字段组", "input", "A framed group of fields under a legend.", demoFieldset},
|
||||
{102, "ColorSlot", "色槽选择", "input", "Pick one of the sixteen palette slots. It follows the palette.", demoColorSlot},
|
||||
{103, "Joystick", "摇杆", "input", "Drag the stick inside its ring; it springs home. Arrows nudge it.", demoJoystick},
|
||||
{103, "Joystick", "操作杆", "input", "Drag the stick inside its ring; it springs home. Arrows nudge it.", demoJoystick},
|
||||
{104, "CheatCode", "秘籍", "input", "Focus it and enter ↑↑↓↓←→←→BA. You know the one.", demoCheatCode},
|
||||
{105, "SlotPicker", "滚轮选择", "input", "Reels that spin by drag, wheel or arrows. Line up three for the jackpot.", demoSlotPicker},
|
||||
{105, "SlotPicker", "转轮选择", "input", "Reels that spin by drag, wheel or arrows. Line up three for the jackpot.", demoSlotPicker},
|
||||
}
|
||||
|
||||
type controlDemo struct {
|
||||
@@ -149,7 +149,7 @@ func demoPinInput(c *ui.Context, log func(string)) {
|
||||
if s.pin == "0451" {
|
||||
log("ACCESS GRANTED")
|
||||
} else {
|
||||
log("WRONG CODE " + s.pin)
|
||||
log("WRONG CODE")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ var deskEntries = []Entry{
|
||||
{467, "DesktopIcon", "桌面图标", "desk", "Click to choose and it inverts; double-click or Enter opens it.", demoDesktopIcon},
|
||||
{468, "Taskbar", "任务栏", "desk", "START, a button for each open window, the tray and the clock.", demoTaskbar},
|
||||
{469, "StartMenu", "开始菜单", "desk", "Programs in cascading menus. Arrows walk in and out; Enter runs one.", demoStartMenu},
|
||||
{470, "AppWindow", "应用窗口", "desk", "Drag a window by its title bar, or focus the bar and use the arrows.", demoAppWindow},
|
||||
{470, "AppWindow", "应用框体", "desk", "Drag a window by its title bar, or focus the bar and use the arrows.", demoAppWindow},
|
||||
{471, "Calculator", "计算器", "desk", "It really adds up. Divide by zero for a GAME OVER; C continues.", demoCalculator},
|
||||
{472, "Notepad", "记事本", "desk", "A text area under FILE and EDIT. TIME/DATE stamps the moment.", demoNotepad},
|
||||
{473, "Minesweeper", "扫雷", "desk", "Open with a click or Enter, flag with a right click or F. The face starts over.", demoMinesweeper},
|
||||
|
||||
@@ -20,11 +20,11 @@ var displayEntries = []Entry{
|
||||
{43, "Picture", "像素图", "display", "Any image reduced to the palette with Bayer dither. Press P: it recolors.", demoPicture},
|
||||
{44, "Avatar", "头像", "display", "A face built from a name: skin, hair, eyes, mouth and background.", demoAvatar},
|
||||
{45, "AvatarGroup", "头像组", "display", "Overlapping faces and a chip for the rest.", demoAvatarGroup},
|
||||
{46, "Badge", "徽标", "display", "A count or a word in a notched pill; counts stop at 99+.", demoBadge},
|
||||
{46, "Badge", "角标", "display", "A count or a word in a notched pill; counts stop at 99+.", demoBadge},
|
||||
{47, "Tag", "标签", "display", "A chip with text, an icon and a remove box.", demoTag},
|
||||
{48, "StatusDot", "状态点", "display", "Online, away, busy or offline; a live online dot blinks.", demoStatusDot},
|
||||
{49, "Medal", "奖牌", "display", "Gold, silver and bronze on ribbons, rank on the face.", demoMedal},
|
||||
{50, "Trophy", "奖杯", "display", "A gold cup over an engraved plaque.", demoTrophy},
|
||||
{49, "Medal", "勋章", "display", "Gold, silver and bronze on ribbons, rank on the face.", demoMedal},
|
||||
{50, "Trophy", "金杯", "display", "A gold cup over an engraved plaque.", demoTrophy},
|
||||
{51, "Stat", "数据", "display", "A big value, its label and a change up or down.", demoStat},
|
||||
{52, "KeyValue", "键值", "display", "A label and a value joined by a dotted leader.", demoKeyValue},
|
||||
{53, "Portrait", "立绘框", "display", "A character in a frame over a nameplate.", demoPortrait},
|
||||
@@ -73,8 +73,8 @@ func demoIcon(c *ui.Context, log func(string)) {
|
||||
func demoSprite(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
ui.Row(c).Gap(k.D(12)).Children(func() {
|
||||
display.Sprite(c, slime, display.SpriteOptions{Scale: 3, FPS: 2, Label: "slime"})
|
||||
display.Sprite(c, slime, display.SpriteOptions{Scale: 3, FPS: 2, Mirror: true, Label: "slime, mirrored"})
|
||||
display.Sprite(c, slime, display.SpriteOptions{Scale: 3, FPS: 4, Label: "slime"})
|
||||
display.Sprite(c, slime, display.SpriteOptions{Scale: 3, FPS: 4, Mirror: true, Label: "slime, mirrored"})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
var feedbackEntries = []Entry{
|
||||
{106, "Progress", "进度条", "feedback", "A bar that fills chunk by chunk. Below zero, a block runs back and forth.", demoProgress},
|
||||
{107, "Spinner", "旋转器", "feedback", "Eight frames of beads going round. It keeps turning under Reduce Motion: it means work.", demoSpinner},
|
||||
{107, "Spinner", "转圈器", "feedback", "Eight frames of beads going round. It keeps turning under Reduce Motion: it means work.", demoSpinner},
|
||||
{108, "Meter", "计量条", "feedback", "A level that turns Warn past a mark and Bad past the next.", demoMeter},
|
||||
{109, "Skeleton", "骨架屏", "feedback", "Dithered stand-ins with a shine sweeping across in steps.", demoSkeleton},
|
||||
{110, "Alert", "提示框", "feedback", "A framed callout: info, success, warning or danger, with an optional ×.", demoAlert},
|
||||
@@ -27,15 +27,15 @@ var feedbackEntries = []Entry{
|
||||
{117, "BarsLoader", "柱状加载", "feedback", "Five bars rising and falling, a step apart.", demoBarsLoader},
|
||||
{118, "Hourglass", "沙漏", "feedback", "Sand falls in steps, then the glass flips.", demoHourglass},
|
||||
{119, "Toast", "吐司", "feedback", "Short messages at the bottom, three at a time, each for a few seconds. Some can be undone.", demoToast},
|
||||
{120, "InlineMessage", "行内提示", "feedback", "A small ! or ✓ note under a field.", demoInlineMessage},
|
||||
{120, "InlineMessage", "行内提示", "feedback", "A small warning or check note under a field.", demoInlineMessage},
|
||||
{121, "CountUp", "计数", "feedback", "Numbers count up in steps, the way scores should.", demoCountUp},
|
||||
{122, "Pulse", "脉冲", "feedback", "A dithered ring grows around what wants attention.", demoPulse},
|
||||
{123, "LevelUp", "升级", "feedback", "LEVEL UP! slides in with sparkles when the level rises.", demoLevelUp},
|
||||
{124, "Achievement", "成就", "feedback", "A trophy card slides in, waits, and slides away.", demoAchievement},
|
||||
{125, "ComboCounter", "连击", "feedback", "×12 COMBO! It pops a dot bigger with every hit.", demoComboCounter},
|
||||
{126, "ContinuePrompt", "继续?", "feedback", "CONTINUE? YES or NO before the count runs out.", demoContinuePrompt},
|
||||
{127, "SaveIndicator", "存档指示", "feedback", "A floppy spins while saving, then SAVED ✓.", demoSaveIndicator},
|
||||
{128, "BusyOverlay", "忙碌遮罩", "feedback", "A checker veil and spinner over busy content; it takes the pointer and the focus.", demoBusyOverlay},
|
||||
{127, "SaveIndicator", "存档指示", "feedback", "A floppy spins while saving, then SAVED with a check.", demoSaveIndicator},
|
||||
{128, "BusyOverlay", "加载遮罩", "feedback", "A checker veil and spinner over busy content; it takes the pointer and the focus.", demoBusyOverlay},
|
||||
{129, "ReadyGo", "预备开始", "feedback", "3, 2, 1, GO! in big digits, a second each.", demoReadyGo},
|
||||
{130, "Shake", "抖动", "feedback", "Jolts its child side to side, for a wrong answer.", demoShake},
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ var filesEntries = []Entry{
|
||||
{409, "PathBar", "路径栏", "files", "Where you are, folder by folder. Click one to climb back up.", demoPathBar},
|
||||
{410, "MemoryCard", "记忆卡", "files", "Fifteen blocks, like the grey card of old. DELETE frees one.", demoMemoryCard},
|
||||
{411, "DiskUsage", "磁盘占用", "files", "Where the room went, stacked in one bar.", demoDiskUsage},
|
||||
{412, "UploadQueue", "上传队列", "files", "Files on their way up. ✕ calls one back.", demoUploadQueue},
|
||||
{412, "UploadQueue", "上传队列", "files", "Files on their way up. × calls one back.", demoUploadQueue},
|
||||
{413, "Trash", "回收站", "files", "A bin that looks full when it is. EMPTY takes it out.", demoTrash},
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ func demoPalettes(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
ui.Grid(c).Columns(2).Gap(k.D(6)).Children(func() {
|
||||
for _, pal := range pixel.Palettes {
|
||||
pixel.Scope(c, pixel.Settings{Palette: pal}, func() {
|
||||
s := pixel.Current(c)
|
||||
s.Palette = pal
|
||||
pixel.Scope(c, s, func() {
|
||||
kp := pixel.K(c)
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element { return ui.ButtonBase(c).Label(pal.Name) })
|
||||
shell.Height(kp.D(52)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
|
||||
+2
-2
@@ -61,7 +61,7 @@ func paletteOf(c *ui.Context, s pixel.Season, night bool) pixel.Settings {
|
||||
|
||||
// over stacks an effect on a same-sized sky.
|
||||
func over(c *ui.Context, w, h int, effect func()) {
|
||||
layout.Layers(c, func() { fx.Sky(c, fx.SkyOptions{Width: w, Height: h}) }, effect)
|
||||
layout.Layers(c, layout.LayersOptions{}, func() { fx.Sky(c, fx.SkyOptions{Width: w, Height: h}) }, effect)
|
||||
}
|
||||
|
||||
// portalBand rings the portal, outside in.
|
||||
@@ -154,7 +154,7 @@ func demoLava(c *ui.Context, log func(string)) {
|
||||
|
||||
func demoAurora(c *ui.Context, log func(string)) {
|
||||
pixel.Scope(c, paletteOf(c, pixel.K(c).Palette().Season, true), func() {
|
||||
layout.Layers(c, func() { fx.Sky(c, fx.SkyOptions{Width: 140, Height: 80}) },
|
||||
layout.Layers(c, layout.LayersOptions{}, func() { fx.Sky(c, fx.SkyOptions{Width: 140, Height: 80}) },
|
||||
func() { fx.Aurora(c, fx.AuroraOptions{Width: 140, Height: 80, Label: "aurora"}) },
|
||||
func() { fx.Mountains(c, fx.MountainsOptions{Width: 140, Height: 80}) })
|
||||
})
|
||||
|
||||
@@ -178,7 +178,7 @@ func demoTimerHUD(c *ui.Context, log func(string)) {
|
||||
s := gameState(c)
|
||||
left := max(0, s.deadline.Sub(k.Now()))
|
||||
if left > 0 {
|
||||
k.Tick(c, 2)
|
||||
k.Tick(c, 4)
|
||||
}
|
||||
ui.Row(c).Gap(k.D(12)).AlignItems(ui.Center).Children(func() {
|
||||
game.TimerHUD(c, left, game.TimerHUDOptions{})
|
||||
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
|
||||
var layoutEntries = []Entry{
|
||||
{1, "Screen", "屏幕", "layout", "The root of a screen: Back fills the window, with an optional dithered vignette.", demoScreen},
|
||||
{2, "Window", "窗口", "layout", "A beveled window with a title tab, the shape of every RPG menu.", demoWindow},
|
||||
{2, "Window", "框体", "layout", "A beveled window with a title tab, the shape of every RPG menu.", demoWindow},
|
||||
{3, "Panel", "面板", "layout", "A box framed raised, sunken, flat, double, window or notched.", demoPanel},
|
||||
{4, "Card", "卡片", "layout", "A raised panel with a header strip, a body and a footer.", demoCard},
|
||||
{5, "Section", "分节", "layout", "A titled group inside a dotted rule.", demoSection},
|
||||
{6, "Divider", "分隔线", "layout", "Rules across or down: solid, dotted, dashed, double, ornament.", demoDivider},
|
||||
{7, "ScrollArea", "滚动区", "layout", "A scroll view with a pixel scrollbar: arrows step, the track pages, the thumb drags.", demoScrollArea},
|
||||
{7, "ScrollArea", "卷动区", "layout", "A scroll view with a pixel scrollbar: arrows step, the track pages, the thumb drags.", demoScrollArea},
|
||||
{8, "Split", "分栏", "layout", "Two panes and a grip. Drag it, or focus it and use the arrow keys.", demoSplit},
|
||||
{9, "Grid", "网格", "layout", "Equal columns with solid or dotted lines between the cells.", demoGrid},
|
||||
{10, "Tiles", "平铺", "layout", "Equal tiles that wrap into rows as the width allows.", demoTiles},
|
||||
@@ -207,7 +207,7 @@ func demoAspectBox(c *ui.Context, log func(string)) {
|
||||
|
||||
func demoLayers(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
layout.Layers(c, func() {
|
||||
layout.Layers(c, layout.LayersOptions{}, func() {
|
||||
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Dither(0, 0, g.W, g.H, pixel.Well, pixel.Info, 6)
|
||||
@@ -251,7 +251,7 @@ func demoTitleBar(c *ui.Context, log func(string)) {
|
||||
func demoStatusBar(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
ui.Box(c).Width(k.D(260)).Children(func() {
|
||||
layout.StatusBar(c, "Ready", "Ln 1, Col 1", "UTF-8")
|
||||
layout.StatusBar(c, []string{"Ready", "Ln 1, Col 1", "UTF-8"}, layout.StatusBarOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ func demoHandheld(c *ui.Context, log func(string)) {
|
||||
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Rect(0, 0, g.W, g.H, pixel.Well)
|
||||
g.Sprite(at[0], at[1], slime, g.Step(2), pixel.Ink, 1)
|
||||
g.Sprite(at[0], at[1], slime, g.Step(4)/2, pixel.Ink, 1)
|
||||
})
|
||||
})
|
||||
moves := map[string][2]int{"up": {0, -4}, "down": {0, 4}, "left": {-4, 0}, "right": {4, 0}}
|
||||
|
||||
@@ -18,7 +18,7 @@ var lootEntries = []Entry{
|
||||
{281, "Crafting", "合成台", "game", "Click a cell to swap ingredients. Herb over water makes a potion.", demoCrafting},
|
||||
{282, "LootPopup", "获得物品", "game", "YOU GOT: something nice! The item rises out of the box.", demoLootPopup},
|
||||
{283, "TreasureChest", "宝箱", "game", "Press the chest. It creaks open; the gem floats up.", demoTreasureChest},
|
||||
{284, "GachaReel", "抽奖转轮", "game", "SPIN: three reels, stopping one by one. No real money involved.", demoGachaReel},
|
||||
{284, "GachaReel", "扭蛋转轮", "game", "SPIN: three reels, stopping one by one. No real money involved.", demoGachaReel},
|
||||
{285, "InteractPrompt", "互动提示", "game", "Walk up to the mayor and a TALK bubble bobs above him.", demoInteractPrompt},
|
||||
{286, "TurnOrder", "行动顺序", "game", "Who acts next. The active one stands raised.", demoTurnOrder},
|
||||
{287, "BattleLog", "战斗日志", "game", "Combat lines roll up as the fight goes on.", demoBattleLog},
|
||||
@@ -222,7 +222,7 @@ func demoStaminaBar(c *ui.Context, log func(string)) {
|
||||
s := lootState(c)
|
||||
now := k.Now()
|
||||
// Sprinting drains a quarter a second; rest refills a tenth.
|
||||
if dt := now.Sub(s.last).Seconds(); s.sprint {
|
||||
if dt := max(0, now.Sub(s.last).Seconds()); s.sprint {
|
||||
s.stamina = max(0, s.stamina-dt/4)
|
||||
} else {
|
||||
s.stamina = min(1, s.stamina+dt/10)
|
||||
|
||||
@@ -16,10 +16,10 @@ import (
|
||||
var marketEntries = []Entry{
|
||||
{444, "Quantity", "数量", "shop", "- and + within limits. The side at its limit fades.", demoQuantity},
|
||||
{445, "PricingTiers", "价格方案", "shop", "Bronze, silver and gold. Gold comes with a hat.", demoPricingTiers},
|
||||
{446, "Coupon", "优惠券", "shop", "A ticket with notched edges. COPY takes the code.", demoCoupon},
|
||||
{447, "DailyReward", "每日奖励", "shop", "Seven days, seven chests. Come back daily; today's is ready.", demoDailyReward},
|
||||
{446, "Coupon", "优惠码", "shop", "A ticket with notched edges. COPY takes the code.", demoCoupon},
|
||||
{447, "DailyReward", "每日福利", "shop", "Seven days, seven chests. Come back daily; today's is ready.", demoDailyReward},
|
||||
{448, "BidTicker", "竞价", "shop", "The latest bids on top; the highest wears the crown.", demoBidTicker},
|
||||
{449, "TradeWindow", "交易窗口", "shop", "Two offers. Both must confirm, and any change takes the confirms back.", demoTradeWindow},
|
||||
{449, "TradeWindow", "交易框", "shop", "Two offers. Both must confirm, and any change takes the confirms back.", demoTradeWindow},
|
||||
{450, "VendingMachine", "自动售货机", "shop", "Type a code like B3 and press OK. Credit first, snacks after.", demoVendingMachine},
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
var mediaEntries = []Entry{
|
||||
{346, "AudioPlayer", "音乐播放器", "media", "A whole deck in one box. Stop rewinds to the top.", demoAudioPlayer},
|
||||
{347, "Transport", "播放控制", "media", "Back, play or pause, stop, skip: the four buttons every deck agrees on.", demoTransport},
|
||||
{348, "Scrubber", "进度拖动", "media", "Drag to seek, or nudge with arrows. Time at both ends.", demoScrubber},
|
||||
{348, "Scrubber", "进度定位", "media", "Drag to seek, or nudge with arrows. Time at both ends.", demoScrubber},
|
||||
{349, "Volume", "音量", "media", "Eight bars; click one or use arrows. The speaker mutes.", demoVolume},
|
||||
{350, "Equalizer", "均衡器", "media", "Five faders, ±12 dB. Bass boost at your own risk.", demoEqualizer},
|
||||
{351, "Visualizer", "频谱", "media", "Bars bounce out from the middle, hottest at the tips.", demoVisualizer},
|
||||
@@ -76,7 +76,8 @@ func beat(s string) [16]bool {
|
||||
func spin(c *ui.Context, last *time.Time, playing bool, at *time.Duration, length time.Duration) {
|
||||
k := pixel.K(c)
|
||||
if playing && !last.IsZero() {
|
||||
*at = (*at + k.Now().Sub(*last)) % length
|
||||
// A clock stepping back plays nothing backward.
|
||||
*at = (*at + max(0, k.Now().Sub(*last))) % length
|
||||
}
|
||||
*last = time.Time{}
|
||||
if playing {
|
||||
|
||||
@@ -16,14 +16,14 @@ var navigationEntries = []Entry{
|
||||
{131, "Tabs", "标签页", "navigation", "Folder tabs; the chosen one joins its panel. Arrows move.", demoTabs},
|
||||
{132, "VerticalTabs", "竖排标签", "navigation", "Tabs down the left edge, for settings screens.", demoVerticalTabs},
|
||||
{133, "Sidebar", "侧边栏", "navigation", "Sections of items with icons and badges. Up and Down choose; one Tab stop.", demoSidebar},
|
||||
{134, "Breadcrumbs", "面包屑", "navigation", "Where you are, split by ›. Every part but the last walks back.", demoBreadcrumbs},
|
||||
{134, "Breadcrumbs", "面包屑", "navigation", "Where you are, split by pointers. Every part but the last walks back.", demoBreadcrumbs},
|
||||
{135, "CommandMenu", "指令菜单", "navigation", "FIGHT, MAGIC, ITEM, RUN, with a bobbing ▶. Enter picks; Escape backs out.", demoCommandMenu},
|
||||
{136, "MenuBar", "菜单栏", "navigation", "A bar of menus. Click one, then the arrows go across and down.", demoMenuBar},
|
||||
{137, "DropdownMenu", "下拉菜单", "navigation", "A button with a menu: icons, rules and shortcut names.", demoDropdownMenu},
|
||||
{138, "ContextMenu", "右键菜单", "navigation", "Right click, or the menu key, opens a menu at the pointer.", demoContextMenu},
|
||||
{139, "Pagination", "分页", "navigation", "◀ 1 … 4 5 6 … 12 ▶. The current page is pressed in.", demoPagination},
|
||||
{140, "PageDots", "页码点", "navigation", "A dot per page; click one, or use the arrows.", demoPageDots},
|
||||
{141, "Steps", "关卡步骤", "navigation", "Flags along a trail: cleared, waving where you are, locked ahead.", demoSteps},
|
||||
{141, "Steps", "关卡阶段", "navigation", "Flags along a trail: cleared, waving where you are, locked ahead.", demoSteps},
|
||||
{142, "NavRail", "导航栏", "navigation", "A rail of icons. Hover or focus one and it says its name.", demoNavRail},
|
||||
{143, "BottomNav", "底部导航", "navigation", "Icons over labels along the bottom, for small screens.", demoBottomNav},
|
||||
{144, "CommandPalette", "命令面板", "navigation", "Cmd+K, type a few letters, press Enter.", demoCommandPalette},
|
||||
@@ -68,7 +68,7 @@ func demoTabs(c *ui.Context, log func(string)) {
|
||||
func demoVerticalTabs(c *ui.Context, log func(string)) {
|
||||
s := navState(c)
|
||||
names := []string{"Video", "Audio", "Controls"}
|
||||
if navigation.VerticalTabs(c, "Settings", names, &s.vtab, navigation.TabsOptions{}, func() {
|
||||
if navigation.VerticalTabs(c, "Settings", names, &s.vtab, navigation.VerticalTabsOptions{}, func() {
|
||||
pixel.Text(c, names[s.vtab]+" settings go here.", pixel.TextOptions{Slot: pixel.Faint})
|
||||
}).Changed {
|
||||
log(names[s.vtab])
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
var overlayEntries = []Entry{
|
||||
{176, "Dialog", "对话框", "overlay", "A titled window over a checkered backdrop. Escape or a click outside closes it.", demoDialog},
|
||||
{177, "AlertDialog", "警告框", "overlay", "Icon, title, message, buttons. Enter presses the highlighted one.", demoAlertDialog},
|
||||
{177, "AlertDialog", "提醒框", "overlay", "Icon, title, message, buttons. Enter presses the highlighted one.", demoAlertDialog},
|
||||
{178, "ConfirmDialog", "确认框", "overlay", "ARE YOU SURE? The hand starts on NO, to be safe.", demoConfirmDialog},
|
||||
{179, "Popover", "弹出层", "overlay", "A panel under its button, notched toward it. It flips above near the bottom.", demoPopover},
|
||||
{180, "Tooltip", "提示气泡", "overlay", "Rest the pointer half a second, or Tab in, and a bubble says what it does.", demoTooltip},
|
||||
|
||||
@@ -111,7 +111,7 @@ func demoTwoFactor(c *ui.Context, log func(string)) {
|
||||
}
|
||||
|
||||
func demoAbout(c *ui.Context, log func(string)) {
|
||||
account.About(c, "8BIT", account.AboutOptions{Version: "1.0.0", Credits: []account.Credit{
|
||||
account.About(c, "8BIT", "1.0.0", account.AboutOptions{Credits: []account.Credit{
|
||||
{Role: "Pixels and code", Name: "The 8bit team"}, {Role: "Native UI", Name: "MyGo"}, {Role: "Font", Name: "Ark Pixel"},
|
||||
}})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
var popupEntries = []Entry{
|
||||
{186, "ChoicePrompt", "选项提示", "overlay", "A question and a hand. Arrows and Enter, or press 1–9.", demoChoicePrompt},
|
||||
{187, "Coachmark", "新手引导", "overlay", "A guided tour: a bubble points at each button in turn.", demoCoachmark},
|
||||
{188, "Spotlight", "聚光灯", "overlay", "Everything but one button sinks into dither.", demoSpotlight},
|
||||
{188, "Spotlight", "探照灯", "overlay", "Everything but one button sinks into dither.", demoSpotlight},
|
||||
{189, "Notification", "通知", "overlay", "Cards stack at the top right and leave on their own. × sends one away.", demoNotification},
|
||||
{190, "Popconfirm", "气泡确认", "overlay", "A small YES / NO bubble before the button acts.", demoPopconfirm},
|
||||
{191, "PauseMenu", "暂停菜单", "overlay", "PAUSED blinks over the game. Escape resumes.", demoPauseMenu},
|
||||
@@ -173,7 +173,7 @@ func demoKeyHints(c *ui.Context, log func(string)) {
|
||||
func demoTitleCard(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := overlayState(c)
|
||||
layout.Layers(c, func() {
|
||||
layout.Layers(c, layout.LayersOptions{}, func() {
|
||||
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Dither(0, 0, g.W, g.H, pixel.Well, pixel.Info, 4)
|
||||
|
||||
@@ -15,7 +15,7 @@ var projectEntries = []Entry{
|
||||
{422, "TaskItem", "任务项", "project", "A box to tick, a title, how urgent, and who.", demoTaskItem},
|
||||
{423, "TaskList", "任务列表", "project", "Tasks with OPEN, DONE and URGENT chips to narrow them down.", demoTaskList},
|
||||
{424, "Milestones", "里程碑", "project", "Flags along the road. The little hero stands at today.", demoMilestones},
|
||||
{425, "Burndown", "燃尽图", "project", "The straight line is the plan. The other one is what happened.", demoBurndown},
|
||||
{425, "Burndown", "冲刺进度", "project", "The straight line is the plan. The other one is what happened.", demoBurndown},
|
||||
{426, "Pomodoro", "番茄钟", "project", "Twenty-five minutes of work, five of rest. The tomato naps on the break.", demoPomodoro},
|
||||
{427, "Priority", "优先级", "project", "LOW, MEDIUM, HIGH and, for the worst, CRITICAL: it is on fire.", demoPriority},
|
||||
{428, "AssigneePicker", "指派", "project", "The party lines up. Pick who takes the quest, or nobody.", demoAssigneePicker},
|
||||
|
||||
@@ -17,13 +17,13 @@ import (
|
||||
|
||||
var recordEntries = []Entry{
|
||||
{164, "StatCompare", "属性对比", "data", "What a new sword does to you: ▲ in green, ▼ in red.", demoStatCompare},
|
||||
{165, "Bestiary", "图鉴", "data", "Monsters you met, and ??? shadows of those you have not.", demoBestiary},
|
||||
{165, "Bestiary", "怪物图录", "data", "Monsters you met, and ??? shadows of those you have not.", demoBestiary},
|
||||
{166, "PropertyGrid", "属性表", "data", "Edit text, numbers and switches row by row.", demoPropertyGrid},
|
||||
{167, "Pager", "分页器", "data", "Pick a page size; ◀ ▶ step through 312 items.", demoPager},
|
||||
{168, "Feed", "动态流", "data", "What happened, newest first, with icons and times.", demoFeed},
|
||||
{169, "Matrix", "矩阵", "data", "Who knows which spell. Arrows move; Space flips.", demoMatrix},
|
||||
{170, "Sortable", "可排序列表", "data", "Drag a quest by its grip, or move it with Alt and the arrows.", demoSortable},
|
||||
{171, "Transfer", "穿梭框", "data", "Pick a hero, then → or ← to swap benches.", demoTransfer},
|
||||
{171, "Transfer", "双栏选择", "data", "Pick a hero, then → or ← to swap benches.", demoTransfer},
|
||||
{172, "InfiniteList", "无限列表", "data", "Scroll to the end; it loads twenty more.", demoInfiniteList},
|
||||
{173, "FilterBar", "筛选栏", "data", "Chips turn filters on; CLEAR turns them all off.", demoFilterBar},
|
||||
{174, "SearchResults", "搜索结果", "data", "Type a spell name; matches light up.", demoSearchResults},
|
||||
@@ -100,7 +100,7 @@ func demoInfiniteList(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
s := dataState(c)
|
||||
// A pretend server answers after one second.
|
||||
if !s.loading.IsZero() && k.Since(c, s.loading, 1, 1) == 1 {
|
||||
if !s.loading.IsZero() && k.Since(c, s.loading, 4, 4) == 4 {
|
||||
s.logs, s.loading = s.logs+20, time.Time{}
|
||||
log(fmt.Sprintf("loaded %d rows", s.logs))
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ func demoDamageNumbers(c *ui.Context, log func(string)) {
|
||||
s, dice := rpgState(c), gameState(c).dice
|
||||
ui.Column(c).Gap(k.D(4)).Children(func() {
|
||||
// The numbers float over a slime having a bad day.
|
||||
layout.Layers(c, func() {
|
||||
layout.Layers(c, layout.LayersOptions{}, func() {
|
||||
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Sprite((g.W-3*slime.W)/2, g.H-3*slime.H, slime, 0, pixel.Ink, 3)
|
||||
|
||||
@@ -127,7 +127,7 @@ func demoGlitch(c *ui.Context, log func(string)) {
|
||||
|
||||
func demoSparkle(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
layout.Layers(c, func() {
|
||||
layout.Layers(c, layout.LayersOptions{}, func() {
|
||||
ui.Box(c).Size(k.D(48), k.D(48)).AlignItems(ui.Center).Justify(ui.Center).Children(func() {
|
||||
display.Gem(c, display.Legendary, display.GemOptions{Scale: 2})
|
||||
})
|
||||
@@ -148,7 +148,7 @@ func demoExplosion(c *ui.Context, log func(string)) {
|
||||
|
||||
func demoConfetti(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
layout.Layers(c, func() {
|
||||
layout.Layers(c, layout.LayersOptions{}, func() {
|
||||
ui.Box(c).Size(k.D(140), k.D(64)).AlignItems(ui.Center).Justify(ui.Center).Children(func() {
|
||||
pixel.Text(c, "YOU WIN!", pixel.TextOptions{Scale: 2, Slot: pixel.Accent})
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@ var studioEntries = []Entry{
|
||||
{359, "Waveform", "波形", "media", "Every sample a line, up or down from zero.", demoWaveform},
|
||||
{360, "VideoFrame", "视频框", "media", "A 16:9 screen with play and a scrubber. The slime does the rest.", demoVideoFrame},
|
||||
{361, "ImageCompare", "对比滑块", "media", "Drag the grip: sketch on the left, paint on the right.", demoImageCompare},
|
||||
{362, "Gallery", "画廊", "media", "Arrows walk the thumbnails; the big picture follows.", demoGallery},
|
||||
{362, "Gallery", "相册", "media", "Arrows walk the thumbnails; the big picture follows.", demoGallery},
|
||||
{363, "Filmstrip", "胶片条", "media", "A sprite's frames on film, sprocket holes and all.", demoFilmstrip},
|
||||
{364, "RecordButton", "录制按钮", "media", "The dot blinks while it records, and the time runs.", demoRecordButton},
|
||||
{365, "Tuner", "调频器", "media", "Drag the needle across FM. A few stations are on the air.", demoTuner},
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
)
|
||||
|
||||
var tabletopEntries = []Entry{
|
||||
{486, "Dice", "骰子", "tabletop", "A d6 with pips. ROLL and it tumbles before it lands.", demoDice},
|
||||
{487, "DiceTray", "骰盘", "tabletop", "Several dice on felt, and what they add up to.", demoDiceTray},
|
||||
{486, "Dice", "色子", "tabletop", "A d6 with pips. ROLL and it tumbles before it lands.", demoDice},
|
||||
{487, "DiceTray", "色子盘", "tabletop", "Several dice on felt, and what they add up to.", demoDiceTray},
|
||||
{488, "PlayingCard", "扑克牌", "tabletop", "Rank and suit, court cards with their pieces, or the back.", demoPlayingCard},
|
||||
{489, "CardHand", "手牌", "tabletop", "A fanned hand. The card you point at or choose lifts; a click plays it.", demoCardHand},
|
||||
{490, "Deck", "牌堆", "tabletop", "A face-down pile with its count. Click to draw.", demoDeck},
|
||||
|
||||
@@ -113,7 +113,7 @@ func demoBlinkText(c *ui.Context, log func(string)) {
|
||||
k := pixel.K(c)
|
||||
ui.Row(c).Gap(k.D(12)).Children(func() {
|
||||
text.BlinkText(c, "PRESS START", text.BlinkTextOptions{Scale: 2})
|
||||
text.BlinkText(c, "LOW HP", text.BlinkTextOptions{Slot: pixel.Bad, FPS: 6})
|
||||
text.BlinkText(c, "LOW HP", text.BlinkTextOptions{Slot: pixel.Bad, FPS: 12})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestGalleryTextHasGlyphs(t *testing.T) {
|
||||
// Every visible word comes from the font, never tofu.
|
||||
check := func(where, s string) {
|
||||
for _, r := range s {
|
||||
if r != ' ' && !pixel.Has(r) {
|
||||
t.Errorf("%s: the font has no %q in %q", where, r, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, e := range Entries() {
|
||||
check(e.Slug(), e.Name+e.Zh+e.Summary)
|
||||
}
|
||||
for _, path := range []string{"/", "/cat/foundations", "/nowhere"} {
|
||||
_, tt := open(path)
|
||||
for _, s := range tt.Texts() {
|
||||
check(path, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/input"
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestEveryFocusHasAName(t *testing.T) {
|
||||
for _, e := range Entries() {
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay})
|
||||
// A named start, so Tab always lands and laps.
|
||||
input.Button(c, "SENTINEL", input.ButtonOptions{})
|
||||
ui.Column(c).Fill().Children(func() { e.Demo(c, func(string) {}) })
|
||||
}, 640, 480)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
for i := range 12 {
|
||||
tt.Key(0, ui.KeyTab)
|
||||
if tt.Focused("SENTINEL") {
|
||||
break
|
||||
}
|
||||
// Focused counts ancestors, so a named focus has a hit.
|
||||
named := false
|
||||
for _, s := range tt.Texts() {
|
||||
named = named || tt.Focused(s)
|
||||
}
|
||||
if !named {
|
||||
t.Errorf("%s: Tab %d focuses something with no name", e.Slug(), i+1)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -52,7 +52,7 @@ func (a *App) home(c *ui.Context) {
|
||||
k := pixel.K(c)
|
||||
p := k.Palette()
|
||||
// The banner lives in the palette's own season and hour.
|
||||
layout.Layers(c, func() {
|
||||
layout.Layers(c, layout.LayersOptions{}, func() {
|
||||
fx.SeasonScene(c, p.Season, hourOf(p), fx.SeasonSceneOptions{Height: 96, Label: p.Name + " scene"}).FillWidth()
|
||||
}, func() {
|
||||
ui.Column(c).Padding(k.D(10)).Children(func() {
|
||||
@@ -94,7 +94,7 @@ func (a *App) palettes(c *ui.Context) {
|
||||
for i, pal := range pixel.Palettes {
|
||||
name := pal.Name
|
||||
if i == a.palette {
|
||||
name += ", in use"
|
||||
name += ", " + strings.ToLower(k.Msg("in_use"))
|
||||
}
|
||||
set := pixel.Current(c)
|
||||
set.Palette = pal
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestVeiledKeysDoNothing(t *testing.T) {
|
||||
// KeyUnknown stands for typed text, "a1".
|
||||
keys := []ui.Key{ui.KeyEnter, ui.KeySpace, ui.KeyUp, ui.KeyDown, ui.KeyLeft, ui.KeyRight, ui.Key1, ui.KeyUnknown}
|
||||
for _, e := range Entries() {
|
||||
for i := range 3 * len(keys) {
|
||||
stop, key := i/len(keys), keys[i%len(keys)]
|
||||
at, inert, logs := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC), false, []string{}
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return at }, Inert: inert})
|
||||
ui.Column(c).Fill().Children(func() { e.Demo(c, func(s string) { logs = append(logs, s) }) })
|
||||
}, 640, 480)
|
||||
// Each key at each of the first three Tab stops.
|
||||
for range 1 + stop {
|
||||
tt.Key(0, ui.KeyTab)
|
||||
}
|
||||
before := len(logs)
|
||||
// The veil falls in the frame the key comes.
|
||||
inert = true
|
||||
if key == ui.KeyUnknown {
|
||||
tt.Type("a1")
|
||||
} else {
|
||||
tt.Key(0, key)
|
||||
}
|
||||
if len(logs) > before {
|
||||
t.Errorf("%s: %v under a falling veil logged %q", e.Slug(), key, logs[before:])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package gallery
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
|
||||
)
|
||||
|
||||
func TestVeiledPointersDoNothing(t *testing.T) {
|
||||
for _, e := range Entries() {
|
||||
for _, at := range [][2]float32{{30, 20}, {120, 40}, {240, 60}, {60, 120}, {200, 160}, {360, 100}, {100, 260}, {300, 320}} {
|
||||
now, inert, logs := time.Date(2026, 10, 12, 12, 0, 0, 0, time.UTC), false, []string{}
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.SpringDay, Now: func() time.Time { return now }, Inert: inert})
|
||||
ui.Column(c).Fill().Children(func() { e.Demo(c, func(s string) { logs = append(logs, s) }) })
|
||||
}, 640, 480)
|
||||
tt.Move(at[0], at[1])
|
||||
before := len(logs)
|
||||
// The veil falls with the click, then with a notch.
|
||||
inert = true
|
||||
tt.ClickAt(at[0], at[1])
|
||||
tt.Scroll(at[0], at[1], 0, 40)
|
||||
if len(logs) > before {
|
||||
t.Errorf("%s: a click or wheel at %v under a falling veil logged %q", e.Slug(), at, logs[before:])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -55,7 +55,7 @@ func Credits(c *ui.Context, text string, credits []Credit, o CreditsOptions) Cre
|
||||
end, roll := h/2+full-pixel.LineHeight/2, start(k, e, o.Cue)
|
||||
top, step := 0, end
|
||||
if !k.Reduce() {
|
||||
step = k.Since(c, roll.at, 16, end)
|
||||
step = k.Since(c, roll.at, 15, end)
|
||||
top = h - step
|
||||
} else {
|
||||
h = full
|
||||
|
||||
@@ -31,13 +31,15 @@ const life = 800 * time.Millisecond
|
||||
func DamageNumbers(c *ui.Context, text string, hits *[]Hit, o DamageNumbersOptions) *ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := pixel.Or("DamageNumbers Width", o.Width, 160), pixel.Or("DamageNumbers Height", o.Height, 80)
|
||||
now := k.Now()
|
||||
// S5: hits rise in whole frames, fifteen a second.
|
||||
now := k.Now().Truncate(time.Second / 15)
|
||||
live := (*hits)[:0]
|
||||
for _, hit := range *hits {
|
||||
if hit.X < 0 || hit.X >= w || hit.Y < 0 || hit.Y >= h {
|
||||
panic(fmt.Sprintf("game: hit at %d,%d is off the %d×%d box", hit.X, hit.Y, w, h))
|
||||
}
|
||||
if hit.at.IsZero() {
|
||||
// A clock stepping back starts a hit again.
|
||||
if hit.at.IsZero() || now.Before(hit.at) {
|
||||
hit.at = now
|
||||
}
|
||||
if now.Sub(hit.at) < life {
|
||||
@@ -54,7 +56,7 @@ func DamageNumbers(c *ui.Context, text string, hits *[]Hit, o DamageNumbersOptio
|
||||
}
|
||||
}
|
||||
if len(shown) > 0 {
|
||||
k.Tick(c, 20)
|
||||
k.Tick(c, 15)
|
||||
}
|
||||
return ui.Box(c).Role(ui.RoleImage).Label(text).Description(strings.Join(parts, ", ")).Size(k.D(w), k.D(h)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ func Equipment(c *ui.Context, text string, gear []data.Item, selected *int, o Eq
|
||||
})
|
||||
arrows(base, spots, selected)
|
||||
res := EquipmentResult{Element: shell, Picked: -1}
|
||||
if base.Shortcut(0, ui.KeyEnter) || base.Shortcut(0, ui.KeySpace) {
|
||||
if pixel.Shortcut(base, 0, ui.KeyEnter) || pixel.Shortcut(base, 0, ui.KeySpace) {
|
||||
res.Picked = *selected
|
||||
}
|
||||
left := (w - (3*pitch - 4)) / 2
|
||||
|
||||
+2
-1
@@ -93,7 +93,7 @@ func arrows(base *ui.Element, spots []Spot, cur *int) {
|
||||
key ui.Key
|
||||
dx, dy int
|
||||
}{{ui.KeyLeft, -1, 0}, {ui.KeyRight, 1, 0}, {ui.KeyUp, 0, -1}, {ui.KeyDown, 0, 1}} {
|
||||
if base.Shortcut(0, m.key) {
|
||||
if pixel.Shortcut(base, 0, m.key) {
|
||||
*cur = nearest(spots, *cur, m.dx, m.dy)
|
||||
}
|
||||
}
|
||||
@@ -120,5 +120,6 @@ func start(k pixel.Kit, e *ui.Element, cue int) begun {
|
||||
if s.cue != cue {
|
||||
s.cue, s.at, s.run = cue, k.Now(), s.run+1
|
||||
}
|
||||
k.Elapsed(&s.at)
|
||||
return *s
|
||||
}
|
||||
+4
-2
@@ -40,7 +40,9 @@ func HealthBar(c *ui.Context, text string, hp, full int, o HealthBarOptions) *ui
|
||||
now := k.Now()
|
||||
// end is the trail's end: held, then draining fast.
|
||||
end := func() float64 {
|
||||
return max(float64(tr.seen), tr.from-max(0, now.Sub(tr.at)-hold).Seconds()*float64(full))
|
||||
// S5: the drain moves in whole frames, fifteen a second.
|
||||
drained := max(0, now.Sub(tr.at)-hold).Truncate(time.Second / 15)
|
||||
return max(float64(tr.seen), tr.from-drained.Seconds()*float64(full))
|
||||
}
|
||||
if hp < tr.seen {
|
||||
tr.from, tr.at = end(), now
|
||||
@@ -54,7 +56,7 @@ func HealthBar(c *ui.Context, text string, hp, full int, o HealthBarOptions) *ui
|
||||
shown = float64(hp)
|
||||
}
|
||||
if shown > float64(hp) {
|
||||
k.Tick(c, 30)
|
||||
k.Tick(c, 15)
|
||||
}
|
||||
// Divide, not multiply: huge HP must not overflow.
|
||||
ink := pixel.Good
|
||||
|
||||
+15
-10
@@ -40,27 +40,32 @@ func Hotbar(c *ui.Context, text string, items []data.Item, selected *int, o Hotb
|
||||
old := *selected
|
||||
shell, base, st, used := pixel.Pad(c, text, n, n, data.SlotSize, data.SlotSize, selected, o.Disabled)
|
||||
for i := range n {
|
||||
if base.Shortcut(0, ui.Key1+ui.Key(i)) {
|
||||
if pixel.Shortcut(base, 0, ui.Key1+ui.Key(i)) {
|
||||
*selected = i
|
||||
}
|
||||
}
|
||||
wh := ui.Local(base, wheelKey{}, func() wheel { return wheel{} })
|
||||
notch := k.D(pixel.LineHeight)
|
||||
// The handler runs before this frame knows it is off.
|
||||
base.HandleInput(func(ev ui.InputEvent) bool {
|
||||
if ev.Kind != ui.InputScroll {
|
||||
return false
|
||||
}
|
||||
// A notch moves one slot; the bar wraps around.
|
||||
for wh.left += ev.DY; wh.left >= notch || wh.left <= -notch; {
|
||||
d := 1
|
||||
if wh.left < 0 {
|
||||
d = -1
|
||||
}
|
||||
wh.left -= float32(d) * notch
|
||||
*selected, wh.moved = ((*selected+d)%n+n)%n, true
|
||||
}
|
||||
wh.left += ev.DY
|
||||
return true
|
||||
})
|
||||
if base.IsDisabled() {
|
||||
wh.left = 0
|
||||
}
|
||||
// A notch moves one slot; the bar wraps around.
|
||||
for wh.left >= notch || wh.left <= -notch {
|
||||
d := 1
|
||||
if wh.left < 0 {
|
||||
d = -1
|
||||
}
|
||||
wh.left -= float32(d) * notch
|
||||
*selected, wh.moved = ((*selected+d)%n+n)%n, true
|
||||
}
|
||||
res := HotbarResult{Element: shell, Changed: *selected != old || wh.moved, Used: used}
|
||||
wh.moved = false
|
||||
base.Description(items[*selected].String())
|
||||
|
||||
@@ -31,7 +31,7 @@ func InteractPrompt(c *ui.Context, target *ui.Element, action string, o Interact
|
||||
at := max(3, int((b.X+b.W/2-x)/k.Dot()))
|
||||
bob := 0
|
||||
if !k.Reduce() {
|
||||
bob = k.Tick(c, 2) % 2
|
||||
bob = k.Tick(c, 4) / 2 % 2
|
||||
}
|
||||
var e *ui.Element
|
||||
ui.Overlay(c, func() {
|
||||
|
||||
Loaded 100 of 198 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user