Upgrade MyGo to v0.5.5
Elements are now pass-scoped value handles: tests that read bounds after a frame bind a ui.Handle. pixel tells frames apart by the build pass's root, since the Tester clock no longer moves on its own. Fix the regressions the upgrade brought: - Clipboard reads in input handlers go through c.Services(). - MyGo notes an overlay's opener after building it, so AutoFocus inside lost it; pixel.AutoFocus focuses through a Handle, and archtest bans the raw call. Dialogs hand the focus back again. - Elements around a press stay hovered: Tooltip tells a press-given focus by focus arriving under the pointer. - The web host moves the Tester clock with real time, at most 500 ms a call, and gives components the real clock. The gallery's source index also accepts CRLF checkouts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1f3d38110e
commit
03e1f91706
532 files changed
+828
-735
No files matched your search
@@ -8,13 +8,13 @@ Progress truth: `TASKS.md`. Requirements: `PRD.md` and `docs/prd/components-*.md
|
||||
| Item | Value |
|
||||
| --- | --- |
|
||||
| Go | 1.27.1 |
|
||||
| MyGo | `github.com/egoist/mygo v0.2.10`, pinned in go.mod |
|
||||
| MyGo | `github.com/egoist/mygo v0.5.5`, pinned in go.mod |
|
||||
| Font | Ark Pixel Font 12px proportional zh_Hans v2026.09.25 (OFL-1.1, `pixel/OFL.txt`). Rebuild: `go run ./tools/fontpack -in ark-pixel-12px-proportional-zh_hans.bdf` (BDF sha256 `535f7024…8c86`) |
|
||||
| QR | `rsc.io/qr v0.2.0` (BSD-3), encoder behind `display.QRCode` |
|
||||
| Web | `cmd/web` built with `GOOS=js GOARCH=wasm`; the browser runs the gallery under `ui.Tester` and blits frames to a canvas |
|
||||
| Web | `cmd/web` built with `GOOS=js GOARCH=wasm`; the browser runs the gallery under `ui.Tester`, whose clock the host moves with real time (at most 500 ms a call), and blits frames to a canvas |
|
||||
| Hosting | Cloudflare Workers static assets from `web/dist`; code on Gitea `git.zacharyzhang.com` |
|
||||
|
||||
Read MyGo source before using an API: `$(go env GOMODCACHE)/github.com/egoist/mygo@v0.2.10` (`ui/*.go`, `docs/ui/*.md`). Never guess.
|
||||
Read MyGo source before using an API: `$(go env GOMODCACHE)/github.com/egoist/mygo@v0.5.5` (`ui/*.go`, `docs/ui/*.md`). Never guess.
|
||||
|
||||
## Layout
|
||||
|
||||
@@ -65,7 +65,7 @@ Imports point down only. Same-tier imports are forbidden. `internal/archtest` en
|
||||
- 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`.
|
||||
- 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: MyGo notes the opener after building the overlay, so focus an element as it appears with `pixel.AutoFocus`, never `AutoFocus` (archtest bans it). While a press is on, the elements around it stay `Hovered`: ask `Pressed`, and tell a press-given focus by focus arriving under the pointer.
|
||||
- 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. 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.
|
||||
@@ -77,7 +77,8 @@ Imports point down only. Same-tier imports are forbidden. `internal/archtest` en
|
||||
- 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, 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.
|
||||
- A `ui.Element` is a handle that expires before the next build pass: never keep one across passes; bind a `ui.Handle` instead (`Handle.Bounds(c)` works between builds). No goroutines touching elements.
|
||||
- `Context` methods answer only during a build: an input handler reads the clipboard through `c.Services()`.
|
||||
- Files under 500 lines. Comments: one line, ten words at most, only when needed. No dead code, no versioned APIs.
|
||||
|
||||
## Gallery entry
|
||||
@@ -87,7 +88,7 @@ Each component adds one `Entry` in `gallery/demos_<category>.go`: ID, Name, Zh,
|
||||
## Tests
|
||||
|
||||
- `go vet ./... && go test ./...` before reporting done.
|
||||
- Behavior tests drive `ui.Tester` and assert values. Timed components take `pixel.Settings.Now` from a test clock. `Tester.Focused(label)` is also true when an ancestor holds the focus.
|
||||
- Behavior tests drive `ui.Tester` and assert values. Timed components take `pixel.Settings.Now` from a test clock. The Tester clock stands still until `Advance`; measure `pixel.Due()` against `tt.Now()`. `Tester.Focused(label)` is also true when an ancestor holds the focus.
|
||||
- `gallery` checks every entry under every palette: only palette colors, whole dots. Demos that call `pixel.Scope`, and the home and entry pages, may show other palettes. `PIXEL_SHOTS=<dir> go test ./gallery -run TestInvariants` writes PNGs for visual review.
|
||||
- `gallery` also sweeps all demos: keys, typing, clicks and the wheel as a veil falls log nothing; a clock stepping back crashes nothing and shows no new negative number; every Tab stop has a name; every gallery word has glyphs; the README's counts match the registry.
|
||||
- Web: `npm install && npm test` in `web/` builds `web/dist` and runs Playwright on a local server; `BASE_URL=<url> npx playwright test` checks a deployed site. Tests read `window.__8bit` (`ready`, `stats()`, `texts()`, `page()`, `find(label)`, `log`). Every page must end without console or page errors; a test that expects some says so in an annotation. The budget test logs first frame, p50 and p95 (Go's share plus the canvas blit) and the wasm's brotli bytes, and holds them to PRD §6.
|
||||
|
||||
@@ -14,7 +14,7 @@ Pixel-art components in the manner of 16-bit consoles, for [MyGo](https://github
|
||||
go get git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI@latest
|
||||
```
|
||||
|
||||
It needs Go 1.27.1 or later and builds on MyGo v0.2.10. MyGo's native UI needs nothing else; on Linux it runs on GTK 3.
|
||||
It needs Go 1.27.1 or later and builds on MyGo v0.5.5. MyGo's native UI needs nothing else; on Linux it runs on GTK 3.
|
||||
|
||||
## A first window
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
| Work | Version | License | What it does here |
|
||||
| --- | --- | --- | --- |
|
||||
| [MyGo](https://github.com/egoist/mygo) | v0.2.10 | MIT | Native UI toolkit the components build on |
|
||||
| [MyGo](https://github.com/egoist/mygo) | v0.5.5 | MIT | Native UI toolkit the components build on |
|
||||
| [Ark Pixel Font](https://github.com/TakWolf/ark-pixel-font), 12px proportional zh_Hans | 2026.09.25 | SIL OFL 1.1 | The pixel font, packed into `pixel/font12.bin` |
|
||||
| [rsc.io/qr](https://pkg.go.dev/rsc.io/qr) | v0.2.0 | BSD-3-Clause | QR encoding for `display.QRCode` |
|
||||
| [go-text/typesetting](https://github.com/go-text/typesetting) | v0.3.5 | Unlicense or BSD-3-Clause; used under BSD-3-Clause | Text shaping inside MyGo |
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ type AboutOptions struct {
|
||||
}
|
||||
|
||||
// About shows the title as a logo, version, then credits.
|
||||
func About(c *ui.Context, title, version string, o AboutOptions) *ui.Element {
|
||||
func About(c *ui.Context, title, version string, o AboutOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if version == "" {
|
||||
panic("account: About needs a version")
|
||||
|
||||
@@ -22,7 +22,7 @@ type AccessibilityOptionsOptions struct {
|
||||
}
|
||||
|
||||
type AccessibilityOptionsResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -24,14 +24,14 @@ func choice(name string, i *int, n int) {
|
||||
func words(k pixel.Kit, key string) []string { return strings.Fields(k.Msg(key)) }
|
||||
|
||||
// caption is a line of micro text named for readers.
|
||||
func caption(c *ui.Context, k pixel.Kit, s string, slot pixel.Slot) *ui.Element {
|
||||
func caption(c *ui.Context, k pixel.Kit, s string, slot pixel.Slot) ui.Element {
|
||||
return ui.Box(c).Size(k.D(pixel.MicroWidth(s)), k.D(5)).Role(ui.RoleText).Label(s).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
k.Canvas(p, r).Micro(0, 0, s, slot, 1)
|
||||
})
|
||||
}
|
||||
|
||||
// mark draws s unnamed, beside the control that names it.
|
||||
func mark(c *ui.Context, k pixel.Kit, s string, slot pixel.Slot, w int) *ui.Element {
|
||||
func mark(c *ui.Context, k pixel.Kit, s string, slot pixel.Slot, w int) ui.Element {
|
||||
return ui.Box(c).Size(k.D(max(w, pixel.Measure(s))), k.D(pixel.LineHeight)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
k.Canvas(p, r).Text(0, 0, s, slot, 1)
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@ type AccountSwitcherOptions struct {
|
||||
}
|
||||
|
||||
type AccountSwitcherResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func AccountSwitcher(c *ui.Context, text string, names [4]string, current *int,
|
||||
label += ", " + k.Msg("in_use")
|
||||
}
|
||||
// The shell draws the card; the base stays bare.
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
shell, base, st := pixel.Shell(c, func() ui.Element {
|
||||
return ui.ButtonBase(c).Role(ui.RoleRadio).Label(label).Disabled(o.Disabled)
|
||||
})
|
||||
if base.Clicked() && *current != i {
|
||||
|
||||
@@ -113,7 +113,7 @@ type CharacterCreatorOptions struct {
|
||||
}
|
||||
|
||||
type CharacterCreatorResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ var pad = []struct {
|
||||
type ControllerMapOptions struct{}
|
||||
|
||||
// ControllerMap draws a pad; labels say what each button does.
|
||||
func ControllerMap(c *ui.Context, text string, labels map[PadButton]string, o ControllerMapOptions) *ui.Element {
|
||||
func ControllerMap(c *ui.Context, text string, labels map[PadButton]string, o ControllerMapOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
var said []string
|
||||
for b := DPad; b <= Select; b++ {
|
||||
|
||||
@@ -15,7 +15,7 @@ type DifficultySelectOptions struct {
|
||||
}
|
||||
|
||||
type DifficultySelectResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ type FeedbackFormOptions struct {
|
||||
}
|
||||
|
||||
type FeedbackFormResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Sent bool
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ type KeyBindingsOptions struct {
|
||||
}
|
||||
|
||||
type KeyBindingsResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func KeyBindings(c *ui.Context, text string, binds []Binding, o KeyBindingsOptio
|
||||
panic("account: KeyBindings has no bindings")
|
||||
}
|
||||
w, rh := pixel.Or("KeyBindings Width", o.Width, 200), pixel.LineHeight+4
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
shell, base, st := pixel.Shell(c, func() ui.Element {
|
||||
return ui.Box(c).Focusable().Role(ui.RoleList).Label(text).Disabled(o.Disabled)
|
||||
})
|
||||
kb := ui.Local(base, bindKey{}, func() capture { return capture{} })
|
||||
|
||||
@@ -19,7 +19,7 @@ type LoginFormOptions struct {
|
||||
}
|
||||
|
||||
type LoginFormResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Submitted bool
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type OptionCyclerOptions struct {
|
||||
}
|
||||
|
||||
type OptionCyclerResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func OptionCycler(c *ui.Context, text string, options []string, selected *int, o
|
||||
longest = max(longest, pixel.Measure(s))
|
||||
}
|
||||
w := pixel.Or("OptionCycler Width", o.Width, longest)
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
shell, base, st := pixel.Shell(c, func() ui.Element {
|
||||
return ui.Box(c).Focusable().Role(ui.RoleStepper).Label(text).Disabled(o.Disabled)
|
||||
})
|
||||
n, old := len(options), *selected
|
||||
|
||||
@@ -13,7 +13,7 @@ type PalettePickerOptions struct {
|
||||
}
|
||||
|
||||
type PalettePickerResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ type PasswordGridOptions struct {
|
||||
}
|
||||
|
||||
type PasswordGridResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Submitted is END on a full password.
|
||||
Changed, Submitted bool
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ type PatchNotesOptions struct {
|
||||
}
|
||||
|
||||
// PatchNotes lists versions, each line tagged ADDED, FIXED or CHANGED.
|
||||
func PatchNotes(c *ui.Context, title string, patches []Patch, o PatchNotesOptions) *ui.Element {
|
||||
func PatchNotes(c *ui.Context, title string, patches []Patch, o PatchNotesOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w := pixel.Or("PatchNotes Width", o.Width, 240)
|
||||
// Tags draw in the main font, so any locale reads.
|
||||
|
||||
@@ -14,7 +14,7 @@ type PlayerSelectOptions struct {
|
||||
}
|
||||
|
||||
type PlayerSelectResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Chosen is Enter, Space or a click on a row.
|
||||
Changed, Chosen bool
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ type ProfileCardOptions struct {
|
||||
}
|
||||
|
||||
// ProfileCard shows a player: face, name, level, title and stats.
|
||||
func ProfileCard(c *ui.Context, p Profile, o ProfileCardOptions) *ui.Element {
|
||||
func ProfileCard(c *ui.Context, p Profile, o ProfileCardOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if p.Level < 1 {
|
||||
panic(fmt.Sprintf("account: ProfileCard level %d", p.Level))
|
||||
|
||||
@@ -27,7 +27,7 @@ type SessionListOptions struct {
|
||||
}
|
||||
|
||||
type SessionListResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// SignedOut is the session signed out this frame, else -1.
|
||||
SignedOut int
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ type SettingsListOptions struct {
|
||||
}
|
||||
|
||||
// SettingsList sets controls beside labels under section titles.
|
||||
func SettingsList(c *ui.Context, text string, sections []SettingsSection, o SettingsListOptions) *ui.Element {
|
||||
func SettingsList(c *ui.Context, text string, sections []SettingsSection, o SettingsListOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
return ui.Column(c).Width(k.D(pixel.Or("SettingsList Width", o.Width, 240))).Gap(k.D(2)).Role(ui.RoleGroup).Label(text).Children(func() {
|
||||
for i, s := range sections {
|
||||
|
||||
@@ -19,7 +19,7 @@ type TwoFactorOptions struct {
|
||||
}
|
||||
|
||||
type TwoFactorResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Submitted is the sixth box filled.
|
||||
Changed, Submitted bool
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ type VolumeMixerOptions struct {
|
||||
}
|
||||
|
||||
type VolumeMixerResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ type AgentCardOptions struct {
|
||||
const cardW, cardH = 136, 48
|
||||
|
||||
// AgentCard is a party card: class, level, context as HP.
|
||||
func AgentCard(c *ui.Context, a Agent, o AgentCardOptions) *ui.Element {
|
||||
func AgentCard(c *ui.Context, a Agent, o AgentCardOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if a.Name == "" || a.Class == "" || a.Level < 1 || a.Window < 1 || a.Used < 0 || a.Used > a.Window {
|
||||
panic(fmt.Sprintf("agent: AgentCard %q, %q, LV %d, %d of %d tokens", a.Name, a.Class, a.Level, a.Used, a.Window))
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ type AgentLogOptions struct {
|
||||
type agentLogKey struct{}
|
||||
|
||||
// AgentLog tells steps as a battle log, the newest typed.
|
||||
func AgentLog(c *ui.Context, text, agent string, steps []LogStep, o AgentLogOptions) *ui.Element {
|
||||
func AgentLog(c *ui.Context, text, agent string, steps []LogStep, o AgentLogOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if agent == "" {
|
||||
panic("agent: AgentLog has no agent")
|
||||
|
||||
@@ -19,7 +19,7 @@ type AgentStatusOptions struct {
|
||||
var halo = ring(17)
|
||||
|
||||
// AgentStatus rings a portrait: idle, working, blocked or done.
|
||||
func AgentStatus(c *ui.Context, text string, s Status, o AgentStatusOptions) *ui.Element {
|
||||
func AgentStatus(c *ui.Context, text string, s Status, o AgentStatusOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
face := portrait("AgentStatus", o.Portrait)
|
||||
word, slot := s.look(k)
|
||||
|
||||
@@ -20,7 +20,7 @@ type ApprovalPromptOptions struct {
|
||||
}
|
||||
|
||||
type ApprovalPromptResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Allowed or Denied is the answer given this frame.
|
||||
Allowed, Denied bool
|
||||
}
|
||||
@@ -81,7 +81,7 @@ func ApprovalPrompt(c *ui.Context, text, command string, o ApprovalPromptOptions
|
||||
}
|
||||
|
||||
// sprite is a 12-dot icon, unnamed, in ink.
|
||||
func sprite(c *ui.Context, k pixel.Kit, sp *pixel.Sprite, ink pixel.Slot) *ui.Element {
|
||||
func sprite(c *ui.Context, k pixel.Kit, sp *pixel.Sprite, ink pixel.Slot) ui.Element {
|
||||
return ui.Box(c).Size(k.D(sp.W), k.D(sp.H)).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
k.Canvas(p, r).Sprite(0, 0, sp, 0, ink, 1)
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ type CheckpointsOptions struct {
|
||||
}
|
||||
|
||||
type CheckpointsResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Restored is the checkpoint chosen this frame, else -1.
|
||||
Restored int
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ type CostCoinsOptions struct {
|
||||
}
|
||||
|
||||
// CostCoins stacks cost as gold dollars, silver dimes, bronze cents.
|
||||
func CostCoins(c *ui.Context, text string, cents int, o CostCoinsOptions) *ui.Element {
|
||||
func CostCoins(c *ui.Context, text string, cents int, o CostCoinsOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if cents < 0 {
|
||||
panic(fmt.Sprintf("agent: CostCoins of %d cents", cents))
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ type DiffScrollOptions struct {
|
||||
}
|
||||
|
||||
// DiffScroll unrolls a parchment of changed files, lines counted.
|
||||
func DiffScroll(c *ui.Context, text string, files []FileDiff, o DiffScrollOptions) *ui.Element {
|
||||
func DiffScroll(c *ui.Context, text string, files []FileDiff, o DiffScrollOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(files) == 0 {
|
||||
panic("agent: DiffScroll has no files")
|
||||
|
||||
@@ -18,7 +18,7 @@ type MemoryCrystalsOptions struct {
|
||||
}
|
||||
|
||||
type MemoryCrystalsResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Picked is the crystal chosen this frame, else -1.
|
||||
Picked int
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ type ModelSelectOptions struct {
|
||||
}
|
||||
|
||||
type ModelSelectResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ type PlanQuestOptions struct {
|
||||
}
|
||||
|
||||
// PlanQuest shows a plan's steps as quest objectives.
|
||||
func PlanQuest(c *ui.Context, title string, steps []PlanStep, o PlanQuestOptions) *ui.Element {
|
||||
func PlanQuest(c *ui.Context, title string, steps []PlanStep, o PlanQuestOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if title == "" || len(steps) == 0 {
|
||||
panic(fmt.Sprintf("agent: PlanQuest %q has %d steps", title, len(steps)))
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ type PromptBoxOptions struct {
|
||||
}
|
||||
|
||||
type PromptBoxResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
// Cast is the prompt cast this frame, else empty.
|
||||
Cast string
|
||||
|
||||
@@ -30,7 +30,7 @@ type partyRow struct {
|
||||
}
|
||||
|
||||
// SubAgentParty draws the summoning tree, each agent with its status.
|
||||
func SubAgentParty(c *ui.Context, text string, party []Member, o SubAgentPartyOptions) *ui.Element {
|
||||
func SubAgentParty(c *ui.Context, text string, party []Member, o SubAgentPartyOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w := pixel.Or("SubAgentParty Width", o.Width, 200)
|
||||
var rows []partyRow
|
||||
|
||||
@@ -24,7 +24,7 @@ var (
|
||||
)
|
||||
|
||||
// ThinkingOrb is a crystal ball that swirls while thinking.
|
||||
func ThinkingOrb(c *ui.Context, text string, thinking bool, o ThinkingOrbOptions) *ui.Element {
|
||||
func ThinkingOrb(c *ui.Context, text string, thinking bool, o ThinkingOrbOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
word := k.Msg("idle")
|
||||
if thinking {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type TokenManaOptions struct {
|
||||
}
|
||||
|
||||
// TokenMana shows a token budget's remainder as MP.
|
||||
func TokenMana(c *ui.Context, text string, used, budget int, o TokenManaOptions) *ui.Element {
|
||||
func TokenMana(c *ui.Context, text string, used, budget int, o TokenManaOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if used < 0 || budget < 1 {
|
||||
panic(fmt.Sprintf("agent: TokenMana %d of %d", used, budget))
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ type ToolCallOptions struct {
|
||||
var glow = []pixel.Slot{pixel.Magic, pixel.Info, pixel.Accent}
|
||||
|
||||
// ToolCall is a spell card: CAST, then OK or FAILED.
|
||||
func ToolCall(c *ui.Context, tool string, out Outcome, o ToolCallOptions) *ui.Element {
|
||||
func ToolCall(c *ui.Context, tool string, out Outcome, o ToolCallOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if tool == "" || out < Casting || out > Failed {
|
||||
panic(fmt.Sprintf("agent: ToolCall %q with outcome %d", tool, out))
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ type AreaChartOptions struct {
|
||||
}
|
||||
|
||||
// AreaChart draws a line with the area below it dithered.
|
||||
func AreaChart(c *ui.Context, text string, labels []string, values []float64, o AreaChartOptions) *ui.Element {
|
||||
func AreaChart(c *ui.Context, text string, labels []string, values []float64, o AreaChartOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("AreaChart", o.Width, o.Height, 160, 80)
|
||||
if len(values) != len(labels) || len(values) < 2 {
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@ type AxisOptions struct {
|
||||
}
|
||||
|
||||
// Axis draws ticks and micro labels, for any chart.
|
||||
func Axis(c *ui.Context, o AxisOptions) *ui.Element {
|
||||
func Axis(c *ui.Context, o AxisOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
n := pixel.Or("Axis Length", o.Length, 120)
|
||||
marks := o.Labels
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ type BarChartOptions struct {
|
||||
}
|
||||
|
||||
// BarChart stands bars on micro-font axes; hover reads a value.
|
||||
func BarChart(c *ui.Context, text string, labels []string, values []float64, o BarChartOptions) *ui.Element {
|
||||
func BarChart(c *ui.Context, text string, labels []string, values []float64, o BarChartOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(labels) != len(values) || len(values) == 0 {
|
||||
panic(fmt.Sprintf("chart: BarChart has %d labels for %d values", len(labels), len(values)))
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ func quartile(sorted []float64, q float64) float64 {
|
||||
}
|
||||
|
||||
// BoxPlot boxes each group's quartiles; whiskers reach its ends.
|
||||
func BoxPlot(c *ui.Context, text string, groups []Series, o BoxPlotOptions) *ui.Element {
|
||||
func BoxPlot(c *ui.Context, text string, groups []Series, o BoxPlotOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(groups) == 0 {
|
||||
panic("chart: BoxPlot has no groups")
|
||||
|
||||
@@ -16,7 +16,7 @@ type BubbleChartOptions struct {
|
||||
}
|
||||
|
||||
// BubbleChart draws points as discs, area by Size.
|
||||
func BubbleChart(c *ui.Context, text string, points []Point, o BubbleChartOptions) *ui.Element {
|
||||
func BubbleChart(c *ui.Context, text string, points []Point, o BubbleChartOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("BubbleChart", o.Width, o.Height, 160, 80)
|
||||
f := fitPoints(points, w, h, 8)
|
||||
|
||||
@@ -14,7 +14,7 @@ type BulletChartOptions struct {
|
||||
}
|
||||
|
||||
// BulletChart lays a bar on rising ranges and a target.
|
||||
func BulletChart(c *ui.Context, text string, value, target float64, ranges []float64, o BulletChartOptions) *ui.Element {
|
||||
func BulletChart(c *ui.Context, text string, value, target float64, ranges []float64, o BulletChartOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(ranges) == 0 {
|
||||
panic("chart: BulletChart needs ranges")
|
||||
|
||||
@@ -20,7 +20,7 @@ type CalendarHeatmapOptions struct {
|
||||
const day = 5
|
||||
|
||||
// CalendarHeatmap rates a year's UTC days 0–4, week by week.
|
||||
func CalendarHeatmap(c *ui.Context, text string, year int, level func(d time.Time) int, o CalendarHeatmapOptions) *ui.Element {
|
||||
func CalendarHeatmap(c *ui.Context, text string, year int, level func(d time.Time) int, o CalendarHeatmapOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
ink := o.Slot
|
||||
if ink == 0 {
|
||||
|
||||
@@ -21,7 +21,7 @@ type CandlestickOptions struct {
|
||||
}
|
||||
|
||||
// Candlestick draws wicks and bodies: up Good, down Bad.
|
||||
func Candlestick(c *ui.Context, text string, candles []Candle, o CandlestickOptions) *ui.Element {
|
||||
func Candlestick(c *ui.Context, text string, candles []Candle, o CandlestickOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(candles) == 0 {
|
||||
panic("chart: Candlestick has no candles")
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ type CrosshairOptions struct {
|
||||
}
|
||||
|
||||
// Crosshair is a line chart; hover crosses the nearest point.
|
||||
func Crosshair(c *ui.Context, text string, labels []string, series []Series, o CrosshairOptions) *ui.Element {
|
||||
func Crosshair(c *ui.Context, text string, labels []string, series []Series, o CrosshairOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Crosshair", o.Width, o.Height, 160, 80)
|
||||
p := fitSeries(labels, series, w, h)
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ type DonutChartOptions struct {
|
||||
}
|
||||
|
||||
// DonutChart is a pie with a hole holding the total.
|
||||
func DonutChart(c *ui.Context, text string, slices []Slice, o DonutChartOptions) *ui.Element {
|
||||
func DonutChart(c *ui.Context, text string, slices []Slice, o DonutChartOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
n := pixel.Or("DonutChart Size", o.Size, 64)
|
||||
total, cuts := shares(slices)
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type FunnelOptions struct {
|
||||
}
|
||||
|
||||
// Funnel narrows stage by stage, each bar with its count.
|
||||
func Funnel(c *ui.Context, text string, stages []Slice, o FunnelOptions) *ui.Element {
|
||||
func Funnel(c *ui.Context, text string, stages []Slice, o FunnelOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(stages) == 0 {
|
||||
panic("chart: Funnel has no stages")
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ type GaugeOptions struct {
|
||||
}
|
||||
|
||||
// Gauge needles a half dial and prints the value.
|
||||
func Gauge(c *ui.Context, text string, value float64, o GaugeOptions) *ui.Element {
|
||||
func Gauge(c *ui.Context, text string, value float64, o GaugeOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
span([]float64{o.Min, o.Max, value})
|
||||
lo, hi := o.Min, o.Max
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ type HeatmapOptions struct {
|
||||
}
|
||||
|
||||
// Heatmap shades cells by dither level between two slots.
|
||||
func Heatmap(c *ui.Context, text string, rows, cols []string, values [][]float64, o HeatmapOptions) *ui.Element {
|
||||
func Heatmap(c *ui.Context, text string, rows, cols []string, values [][]float64, o HeatmapOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(values) != len(rows) {
|
||||
panic(fmt.Sprintf("chart: Heatmap has %d rows of values for %d rows", len(values), len(rows)))
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ type HistogramOptions struct {
|
||||
}
|
||||
|
||||
// Histogram counts values into bins, side by side.
|
||||
func Histogram(c *ui.Context, text string, values []float64, o HistogramOptions) *ui.Element {
|
||||
func Histogram(c *ui.Context, text string, values []float64, o HistogramOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
n := pixel.Or("Histogram Bins", o.Bins, 8)
|
||||
w, h := size("Histogram", o.Width, o.Height, 160, 80)
|
||||
|
||||
@@ -16,7 +16,7 @@ type HorizontalBarOptions struct {
|
||||
}
|
||||
|
||||
// HorizontalBar lays labeled bars across, longest to the edge.
|
||||
func HorizontalBar(c *ui.Context, text string, labels []string, values []float64, o HorizontalBarOptions) *ui.Element {
|
||||
func HorizontalBar(c *ui.Context, text string, labels []string, values []float64, o HorizontalBarOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(labels) != len(values) || len(values) == 0 {
|
||||
panic(fmt.Sprintf("chart: HorizontalBar has %d labels for %d values", len(labels), len(values)))
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ type LegendOptions struct {
|
||||
}
|
||||
|
||||
type LegendResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func Legend(c *ui.Context, text string, series []Series, hidden []bool, o Legend
|
||||
row.Children(func() {
|
||||
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) })
|
||||
shell, base, st := pixel.Shell(c, func() ui.Element { return ui.ToggleBase(c, &shown).Label(s.Name).Disabled(o.Disabled) })
|
||||
// The base turns before it learns it is off.
|
||||
if base.Changed() && !base.IsDisabled() {
|
||||
hidden[i], res.Changed = !shown, true
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type LineChartOptions struct {
|
||||
}
|
||||
|
||||
// LineChart draws series as Bresenham lines with square markers.
|
||||
func LineChart(c *ui.Context, text string, labels []string, series []Series, o LineChartOptions) *ui.Element {
|
||||
func LineChart(c *ui.Context, text string, labels []string, series []Series, o LineChartOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("LineChart", o.Width, o.Height, 160, 80)
|
||||
p := fitSeries(labels, series, w, h)
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ type PieChartOptions struct {
|
||||
}
|
||||
|
||||
// PieChart rasterizes one sector per slice, dot by dot.
|
||||
func PieChart(c *ui.Context, text string, slices []Slice, o PieChartOptions) *ui.Element {
|
||||
func PieChart(c *ui.Context, text string, slices []Slice, o PieChartOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
n := pixel.Or("PieChart Size", o.Size, 64)
|
||||
_, cuts := shares(slices)
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ type PolarAreaOptions struct {
|
||||
}
|
||||
|
||||
// PolarArea gives slices equal turns, reaching out by value.
|
||||
func PolarArea(c *ui.Context, text string, slices []Slice, o PolarAreaOptions) *ui.Element {
|
||||
func PolarArea(c *ui.Context, text string, slices []Slice, o PolarAreaOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
n := pixel.Or("PolarArea Size", o.Size, 64)
|
||||
shares(slices)
|
||||
|
||||
@@ -16,7 +16,7 @@ type ProgressRingOptions struct {
|
||||
}
|
||||
|
||||
// ProgressRing fills a ring clockwise from twelve, in pixel arcs.
|
||||
func ProgressRing(c *ui.Context, text string, value float64, o ProgressRingOptions) *ui.Element {
|
||||
func ProgressRing(c *ui.Context, text string, value float64, o ProgressRingOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if !(value >= 0 && value <= 1) {
|
||||
panic(fmt.Sprintf("chart: ProgressRing value %v outside 0–1", value))
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type PyramidOptions struct {
|
||||
}
|
||||
|
||||
// Pyramid sets two series back to back by group.
|
||||
func Pyramid(c *ui.Context, text string, groups []string, left, right Series, o PyramidOptions) *ui.Element {
|
||||
func Pyramid(c *ui.Context, text string, groups []string, left, right Series, o PyramidOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(left.Values) != len(groups) || len(right.Values) != len(groups) {
|
||||
panic(fmt.Sprintf("chart: Pyramid has %d groups for %d and %d values", len(groups), len(left.Values), len(right.Values)))
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ type RadarChartOptions struct {
|
||||
}
|
||||
|
||||
// RadarChart draws a stat polygon on a web of axes.
|
||||
func RadarChart(c *ui.Context, text string, axes []string, values []float64, o RadarChartOptions) *ui.Element {
|
||||
func RadarChart(c *ui.Context, text string, axes []string, values []float64, o RadarChartOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(axes) != len(values) || len(axes) < 3 {
|
||||
panic(fmt.Sprintf("chart: RadarChart has %d axes for %d values", len(axes), len(values)))
|
||||
|
||||
@@ -13,7 +13,7 @@ type ScatterPlotOptions struct {
|
||||
}
|
||||
|
||||
// ScatterPlot puts each point as a 2×2 square.
|
||||
func ScatterPlot(c *ui.Context, text string, points []Point, o ScatterPlotOptions) *ui.Element {
|
||||
func ScatterPlot(c *ui.Context, text string, points []Point, o ScatterPlotOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("ScatterPlot", o.Width, o.Height, 160, 80)
|
||||
f := fitPoints(points, w, h, 2)
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ type SparklineOptions struct {
|
||||
}
|
||||
|
||||
// Sparkline is a line one text line high, end marked.
|
||||
func Sparkline(c *ui.Context, text string, values []float64, o SparklineOptions) *ui.Element {
|
||||
func Sparkline(c *ui.Context, text string, values []float64, o SparklineOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w := pixel.Or("Sparkline Width", o.Width, 48)
|
||||
if len(values) < 2 {
|
||||
|
||||
@@ -24,7 +24,7 @@ type peaks struct {
|
||||
}
|
||||
|
||||
// SpectrumBars lights segmented bars; peak dots fall behind them.
|
||||
func SpectrumBars(c *ui.Context, text string, levels []float64, o SpectrumBarsOptions) *ui.Element {
|
||||
func SpectrumBars(c *ui.Context, text string, levels []float64, o SpectrumBarsOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
n := pixel.Or("SpectrumBars Segments", o.Segments, 10)
|
||||
for _, l := range levels {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type StackedBarOptions struct {
|
||||
}
|
||||
|
||||
// StackedBar stacks each category's series, one slot per series.
|
||||
func StackedBar(c *ui.Context, text string, labels []string, series []Series, o StackedBarOptions) *ui.Element {
|
||||
func StackedBar(c *ui.Context, text string, labels []string, series []Series, o StackedBarOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(labels) == 0 {
|
||||
panic("chart: StackedBar has no labels")
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ type StepChartOptions struct {
|
||||
}
|
||||
|
||||
// StepChart holds each value flat, then steps to the next.
|
||||
func StepChart(c *ui.Context, text string, labels []string, values []float64, o StepChartOptions) *ui.Element {
|
||||
func StepChart(c *ui.Context, text string, labels []string, values []float64, o StepChartOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("StepChart", o.Width, o.Height, 160, 80)
|
||||
p := fitSeries(labels, []Series{{Name: text, Values: values}}, w, h)
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ type TreemapOptions struct {
|
||||
type box struct{ x, y, w, h int }
|
||||
|
||||
// Treemap tiles slices by value in squarified rectangles.
|
||||
func Treemap(c *ui.Context, text string, slices []Slice, o TreemapOptions) *ui.Element {
|
||||
func Treemap(c *ui.Context, text string, slices []Slice, o TreemapOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := size("Treemap", o.Width, o.Height, 160, 80)
|
||||
total, _ := shares(slices)
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type WaterfallOptions struct {
|
||||
}
|
||||
|
||||
// Waterfall walks a running total: Good up, Bad down.
|
||||
func Waterfall(c *ui.Context, text string, labels []string, deltas []float64, o WaterfallOptions) *ui.Element {
|
||||
func Waterfall(c *ui.Context, text string, labels []string, deltas []float64, o WaterfallOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if len(labels) != len(deltas) || len(deltas) == 0 {
|
||||
panic(fmt.Sprintf("chart: Waterfall has %d labels for %d steps", len(labels), len(deltas)))
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
type WinLossOptions struct{}
|
||||
|
||||
// WinLoss rows blocks: wins up, losses down, draws dashed.
|
||||
func WinLoss(c *ui.Context, text string, results []int, o WinLossOptions) *ui.Element {
|
||||
func WinLoss(c *ui.Context, text string, results []int, o WinLossOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
return ui.Box(c).Size(k.D(len(results)*5), k.D(pixel.LineHeight)).Role(ui.RoleImage).Label(text).
|
||||
Description(record(results)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
|
||||
@@ -21,7 +21,7 @@ type AttachmentChipOptions struct {
|
||||
}
|
||||
|
||||
type AttachmentChipResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Removed bool
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ type CallControlsOptions struct {
|
||||
}
|
||||
|
||||
type CallControlsResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed, HungUp bool
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ func CallControls(c *ui.Context, text string, muted, camera *bool, o CallControl
|
||||
k := pixel.K(c)
|
||||
var res CallControlsResult
|
||||
// A slash crosses a sprite that is off.
|
||||
button := func(base func() *ui.Element, sp *pixel.Sprite, fill, ink pixel.Slot, off bool) bool {
|
||||
shell, b, st := pixel.Shell(c, func() *ui.Element { return base().Disabled(o.Disabled).FocusRing(false) })
|
||||
button := func(base func() ui.Element, sp *pixel.Sprite, fill, ink pixel.Slot, off bool) bool {
|
||||
shell, b, st := pixel.Shell(c, func() ui.Element { return base().Disabled(o.Disabled).FocusRing(false) })
|
||||
shell.Size(k.D(22), k.D(22)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
g := k.Canvas(p, r)
|
||||
g.Frame(0, 0, 22, 22, pixel.Raised, fill)
|
||||
@@ -39,15 +39,15 @@ func CallControls(c *ui.Context, text string, muted, camera *bool, o CallControl
|
||||
}
|
||||
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, &m).Label(k.Msg("mute")) }, icon.Mic, pixel.Face, pixel.Ink, *muted) {
|
||||
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, &cam).Label(k.Msg("camera")) }, icon.Camera, pixel.Face, pixel.Ink, !*camera) {
|
||||
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) {
|
||||
if button(func() ui.Element { return ui.ButtonBase(c).Label(k.Msg("hang_up")) }, icon.Phone, pixel.Bad, pixel.AccentInk, false) {
|
||||
res.HungUp = true
|
||||
slog.Debug("8bit: call hung up", "label", text)
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ type Channel struct {
|
||||
}
|
||||
|
||||
type ChannelListResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Submitted is Enter or a double click on a row.
|
||||
Changed, Submitted bool
|
||||
}
|
||||
|
||||
+3
-3
@@ -20,7 +20,7 @@ type Message struct {
|
||||
|
||||
// listed is a chat list's choice; Submitted is Enter.
|
||||
type listed struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed, Submitted bool
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ func list(c *ui.Context, k pixel.Kit, text string, n int, selected *int, w, h in
|
||||
}
|
||||
|
||||
// para wraps s to w dots, hugging its lines.
|
||||
func para(c *ui.Context, k pixel.Kit, s string, w, maxLines int, ink pixel.Slot) *ui.Element {
|
||||
func para(c *ui.Context, k pixel.Kit, s string, w, maxLines int, ink pixel.Slot) ui.Element {
|
||||
lines := pixel.Wrap(s, w, maxLines)
|
||||
lw := 0
|
||||
for _, l := range lines {
|
||||
@@ -75,7 +75,7 @@ func tagW(s string) int {
|
||||
}
|
||||
|
||||
// rule centers label on a line across w dots.
|
||||
func rule(c *ui.Context, k pixel.Kit, label string, w int, ink, edge pixel.Slot) *ui.Element {
|
||||
func rule(c *ui.Context, k pixel.Kit, label string, w int, ink, edge pixel.Slot) ui.Element {
|
||||
tw := tagW(label)
|
||||
if w < tw+16 {
|
||||
panic(fmt.Sprintf("chat: a %d-dot rule cannot hold %q", w, label))
|
||||
|
||||
+1
-1
@@ -262,7 +262,7 @@ func TestDateSeparatorNamesTheDay(t *testing.T) {
|
||||
day := wd.clk.t
|
||||
pixel.ResetDue()
|
||||
tt := view(400, 100, wd, func(c *ui.Context) { chat.DateSeparator(c, day, chat.DateSeparatorOptions{}) })
|
||||
if until := time.Until(pixel.Due()); until <= 0 || until > 2*time.Second {
|
||||
if until := pixel.Due().Sub(tt.Now()); until <= 0 || until > 2*time.Second {
|
||||
t.Fatalf("the next build is due in %v, not at midnight", until)
|
||||
}
|
||||
wd.clk.t = wd.clk.t.Add(2 * time.Second)
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ type Conversation struct {
|
||||
}
|
||||
|
||||
type ChatListResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Submitted is Enter or a double click on a row.
|
||||
Changed, Submitted bool
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ type ChatLogOptions struct {
|
||||
type logKey struct{}
|
||||
|
||||
// ChatLog scrolls messages and follows the end while there.
|
||||
func ChatLog(c *ui.Context, text string, o ChatLogOptions, content func()) *ui.Element {
|
||||
func ChatLog(c *ui.Context, text string, o ChatLogOptions, content func()) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w, h := pixel.Or("ChatLog Width", o.Width, 220), pixel.Or("ChatLog Height", o.Height, 140)
|
||||
box := ui.Box(c).Size(k.D(w), k.D(h))
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ type ComposerOptions struct {
|
||||
}
|
||||
|
||||
type ComposerResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
// Sent is the text sent this frame, else empty.
|
||||
Sent string
|
||||
|
||||
@@ -16,7 +16,7 @@ type DateSeparatorOptions struct {
|
||||
}
|
||||
|
||||
// DateSeparator rules a line with TODAY, YESTERDAY or the date.
|
||||
func DateSeparator(c *ui.Context, day time.Time, o DateSeparatorOptions) *ui.Element {
|
||||
func DateSeparator(c *ui.Context, day time.Time, o DateSeparatorOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
now := k.Now()
|
||||
// At midnight today becomes yesterday.
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type EmojiPickerOptions struct {
|
||||
}
|
||||
|
||||
type EmojiPickerResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Picked is the emoji chosen this frame, else empty.
|
||||
Picked string
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ type LetterOptions struct {
|
||||
}
|
||||
|
||||
// Letter sets a body on parchment with a signature.
|
||||
func Letter(c *ui.Context, body string, o LetterOptions) *ui.Element {
|
||||
func Letter(c *ui.Context, body string, o LetterOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w := pixel.Or("Letter Width", o.Width, 180)
|
||||
if w < 40 {
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ type LinkPreviewOptions struct {
|
||||
}
|
||||
|
||||
type LinkPreviewResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Clicked bool
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func LinkPreview(c *ui.Context, url, title, site string, o LinkPreviewOptions) L
|
||||
// The site sits under the title; the card fits both.
|
||||
sy := 4 + len(lines)*pixel.LineHeight
|
||||
h := max(40, sy+12)
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
shell, base, st := pixel.Shell(c, func() ui.Element {
|
||||
return ui.Box(c).Focusable().Role(ui.RoleLink).Label(title).Description(url).Disabled(o.Disabled).Cursor(ui.CursorPointer).FocusRing(false)
|
||||
})
|
||||
shell.Size(k.D(w), k.D(h)).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ type MailboxOptions struct {
|
||||
}
|
||||
|
||||
type MailboxResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
// Opened is the mail opened this frame, else -1.
|
||||
Opened int
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ type MailReaderOptions struct {
|
||||
}
|
||||
|
||||
// MailReader shows a mail's headers, a rule, its body.
|
||||
func MailReader(c *ui.Context, m Mail, o MailReaderOptions) *ui.Element {
|
||||
func MailReader(c *ui.Context, m Mail, o MailReaderOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w := pixel.Or("MailReader Width", o.Width, 220)
|
||||
return ui.Column(c).Width(k.D(w)).Gap(k.D(4)).Children(func() {
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ type MemberListOptions struct {
|
||||
}
|
||||
|
||||
// MemberList groups members online and offline, with status dots.
|
||||
func MemberList(c *ui.Context, text string, members []Member, o MemberListOptions) *ui.Element {
|
||||
func MemberList(c *ui.Context, text string, members []Member, o MemberListOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w := pixel.Or("MemberList Width", o.Width, 140)
|
||||
var on, off []Member
|
||||
|
||||
@@ -19,7 +19,7 @@ type MentionInputOptions struct {
|
||||
}
|
||||
|
||||
type MentionInputResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed, Submitted, Accepted bool
|
||||
// Mentioned is the member just completed, else empty.
|
||||
Mentioned string
|
||||
|
||||
@@ -16,7 +16,7 @@ type NotificationBellOptions struct {
|
||||
}
|
||||
|
||||
type NotificationBellResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Clicked bool
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func NotificationBell(c *ui.Context, text string, n int, o NotificationBellOptio
|
||||
if n < 0 {
|
||||
panic(fmt.Sprintf("chat: NotificationBell count %d", n))
|
||||
}
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
shell, base, st := pixel.Shell(c, func() ui.Element {
|
||||
return ui.ButtonBase(c).Label(text).Description(fmt.Sprintf(k.Msg("new_count"), n)).Disabled(o.Disabled).FocusRing(false)
|
||||
})
|
||||
s := ui.Local(shell, bellKey{}, func() bellState { return bellState{last: n} })
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ type PollOptions struct {
|
||||
}
|
||||
|
||||
type PollResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Voted is the option voted for this frame, else -1.
|
||||
Voted int
|
||||
}
|
||||
@@ -65,7 +65,7 @@ func Poll(c *ui.Context, question string, opts []PollOption, mine *int, o PollOp
|
||||
// bar fills an option's share; ▶ marks your vote.
|
||||
func bar(c *ui.Context, k pixel.Kit, op PollOption, total int, mine bool, w int, disabled bool) bool {
|
||||
label := fmt.Sprintf("%s, %d", op.Text, op.Votes)
|
||||
shell, base, st := pixel.Shell(c, func() *ui.Element {
|
||||
shell, base, st := pixel.Shell(c, func() ui.Element {
|
||||
return ui.ButtonBase(c).Label(label).Disabled(disabled).FocusRing(false)
|
||||
})
|
||||
n := strconv.Itoa(op.Votes)
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@ type ReactionsOptions struct {
|
||||
}
|
||||
|
||||
type ReactionsResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Toggled is the chip clicked this frame, else -1.
|
||||
Toggled int
|
||||
}
|
||||
@@ -40,7 +40,7 @@ func Reactions(c *ui.Context, text string, rs []Reaction, o ReactionsOptions) Re
|
||||
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 {
|
||||
shell, base, st := pixel.Shell(c, func() ui.Element {
|
||||
return ui.ToggleBase(c, &v).Label(fmt.Sprintf("%s %d", r.Emoji, r.Count)).Disabled(o.Disabled).FocusRing(false)
|
||||
})
|
||||
if v != r.Mine && !base.IsDisabled() {
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ var check = pixel.MustSprite(`
|
||||
`)
|
||||
|
||||
// ReadReceipt shows one check once sent, two once read.
|
||||
func ReadReceipt(c *ui.Context, read bool, o ReadReceiptOptions) *ui.Element {
|
||||
func ReadReceipt(c *ui.Context, read bool, o ReadReceiptOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
sc := pixel.Or("ReadReceipt Scale", o.Scale, 1)
|
||||
if sc > 4 {
|
||||
|
||||
@@ -14,7 +14,7 @@ type SpeechBubbleOptions struct {
|
||||
}
|
||||
|
||||
// SpeechBubble shows who said what, when; tail their side.
|
||||
func SpeechBubble(c *ui.Context, m Message, o SpeechBubbleOptions) *ui.Element {
|
||||
func SpeechBubble(c *ui.Context, m Message, o SpeechBubbleOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
w := pixel.Or("SpeechBubble Width", o.Width, 140)
|
||||
// Yours sit right on Accent, written in AccentInk.
|
||||
|
||||
@@ -14,7 +14,7 @@ type StickerPickerOptions struct {
|
||||
}
|
||||
|
||||
type StickerPickerResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Picked is the sticker chosen this frame, else empty.
|
||||
Picked string
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ var diamond = pixel.MustSprite(`
|
||||
`)
|
||||
|
||||
// SystemMessage centers a notice, such as PLAYER 2 HAS JOINED.
|
||||
func SystemMessage(c *ui.Context, text string, o SystemMessageOptions) *ui.Element {
|
||||
func SystemMessage(c *ui.Context, text string, o SystemMessageOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
ink := o.Slot
|
||||
if ink == pixel.Ink {
|
||||
|
||||
@@ -15,7 +15,7 @@ type TypingIndicatorOptions struct {
|
||||
}
|
||||
|
||||
// TypingIndicator bounces three dots in a bubble while name types.
|
||||
func TypingIndicator(c *ui.Context, name string, o TypingIndicatorOptions) *ui.Element {
|
||||
func TypingIndicator(c *ui.Context, name string, o TypingIndicatorOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
ink := o.Slot
|
||||
if ink == pixel.Ink {
|
||||
|
||||
@@ -12,7 +12,7 @@ type UnreadDividerOptions struct {
|
||||
}
|
||||
|
||||
// UnreadDivider rules NEW MESSAGES in Bad above the first unread.
|
||||
func UnreadDivider(c *ui.Context, o UnreadDividerOptions) *ui.Element {
|
||||
func UnreadDivider(c *ui.Context, o UnreadDividerOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
return rule(c, k, k.Msg("new_messages"), pixel.Or("UnreadDivider Width", o.Width, 200), pixel.Bad, pixel.Bad)
|
||||
}
|
||||
+1
-1
@@ -21,7 +21,7 @@ type UserCardOptions struct {
|
||||
}
|
||||
|
||||
type UserCardResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Clicked is the action clicked this frame, else -1.
|
||||
Clicked int
|
||||
}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ type VoiceMeterOptions struct {
|
||||
}
|
||||
|
||||
// VoiceMeter lights bars for 0–1: green, amber, then red.
|
||||
func VoiceMeter(c *ui.Context, text string, level float64, o VoiceMeterOptions) *ui.Element {
|
||||
func VoiceMeter(c *ui.Context, text string, level float64, o VoiceMeterOptions) ui.Element {
|
||||
k := pixel.K(c)
|
||||
if math.IsNaN(level) || level < 0 || level > 1 {
|
||||
panic(fmt.Sprintf("chat: VoiceMeter level %v outside 0–1", level))
|
||||
|
||||
+20
-2
@@ -20,6 +20,9 @@ import (
|
||||
// dot is DIPs per dot; each dot is one pixel.
|
||||
const dot = gallery.Dot
|
||||
|
||||
// catchUp caps the replay after a hidden tab.
|
||||
const catchUp = 500 * time.Millisecond
|
||||
|
||||
type host struct {
|
||||
app *gallery.App
|
||||
tt *ui.Tester
|
||||
@@ -29,10 +32,14 @@ type host struct {
|
||||
over bool
|
||||
dead bool
|
||||
began time.Time
|
||||
// wall is when the Tester clock last caught up.
|
||||
wall time.Time
|
||||
}
|
||||
|
||||
func main() {
|
||||
h := &host{app: gallery.New("/")}
|
||||
// Components keep real time while the Tester clock lags.
|
||||
h.app.SetClock(time.Now)
|
||||
api := js.Global().Get("Object").New()
|
||||
export := func(name string, fn func(args []js.Value) any) {
|
||||
api.Set(name, js.FuncOf(func(_ js.Value, args []js.Value) any { return h.guard(name, args, fn) }))
|
||||
@@ -84,6 +91,7 @@ func (h *host) guard(name string, args []js.Value, fn func([]js.Value) any) (res
|
||||
// An empty clipboard after a call means nothing was copied.
|
||||
if h.tt != nil {
|
||||
h.tt.SetClipboard("")
|
||||
h.sync()
|
||||
}
|
||||
pixel.ResetDue()
|
||||
h.began = time.Now()
|
||||
@@ -104,7 +112,7 @@ func (h *host) crash(why string) (res any) {
|
||||
}
|
||||
}()
|
||||
h.tt = ui.NewTester(func(c *ui.Context) {
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.WinterNight, Dot: dot})
|
||||
pixel.Use(c, pixel.Settings{Palette: pixel.WinterNight, Dot: dot, Now: time.Now})
|
||||
ui.Column(c).Fill().Center().Children(func() {
|
||||
if feedback.ErrorState(c, why, feedback.ErrorStateOptions{}).Continue {
|
||||
js.Global().Get("location").Call("reload")
|
||||
@@ -112,6 +120,7 @@ func (h *host) crash(why string) (res any) {
|
||||
})
|
||||
}, h.w*dot, h.h*dot)
|
||||
h.tt.SetScale(1.0 / dot)
|
||||
h.wall = time.Now()
|
||||
return h.out()
|
||||
}
|
||||
|
||||
@@ -123,10 +132,18 @@ func (h *host) start(a []js.Value) any {
|
||||
}
|
||||
h.tt = ui.NewTester(h.app.View, h.w*dot, h.h*dot)
|
||||
h.tt.SetScale(1.0 / dot)
|
||||
h.wall = time.Now()
|
||||
slog.Info("8bit: start", "dots", fmt.Sprintf("%dx%d", h.w, h.h), "path", h.app.Path())
|
||||
return h.out()
|
||||
}
|
||||
|
||||
// sync moves the Tester clock on with the real one.
|
||||
func (h *host) sync() {
|
||||
now := time.Now()
|
||||
h.tt.Advance(min(now.Sub(h.wall), catchUp))
|
||||
h.wall = now
|
||||
}
|
||||
|
||||
func (h *host) size(w, ht int) {
|
||||
h.w, h.h = w, ht
|
||||
h.tt.SetSize(w*dot, ht*dot)
|
||||
@@ -169,7 +186,8 @@ func (h *host) out() any {
|
||||
took := float64(time.Since(h.began).Microseconds()) / 1000
|
||||
res := map[string]any{"w": img.Rect.Dx(), "h": img.Rect.Dy(), "pix": pix, "path": h.app.Path(), "cursor": int(h.tt.Cursor()), "took": took}
|
||||
if due := pixel.Due(); !due.IsZero() {
|
||||
res["due"] = float64(time.Until(due).Milliseconds())
|
||||
// Asked on the Tester clock, which may lag.
|
||||
res["due"] = float64(due.Sub(h.tt.Now()).Milliseconds())
|
||||
}
|
||||
if r, ok := h.tt.TextCaret(); ok {
|
||||
res["caret"] = []any{float64(r.X / dot), float64(r.Y / dot), float64(r.H / dot)}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ type BreakpointsOptions struct {
|
||||
}
|
||||
|
||||
type BreakpointsResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Changed bool
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ type CodeBlockOptions struct {
|
||||
}
|
||||
|
||||
type CodeBlockResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Copied bool
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ type CommandHistoryOptions struct {
|
||||
}
|
||||
|
||||
type CommandHistoryResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
// Rerun is Enter or a double click on a command.
|
||||
Changed, Rerun bool
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ type CrashScreenOptions struct {
|
||||
}
|
||||
|
||||
type CrashScreenResult struct {
|
||||
Element *ui.Element
|
||||
Element ui.Element
|
||||
Continue bool
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 532 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user