Twenty account components: SettingsList, OptionCycler, KeyBindings, ControllerMap, ProfileCard, CharacterCreator, LoginForm, PasswordGrid, SessionList, LanguagePicker, PalettePicker, VolumeMixer, DifficultySelect, AccessibilityOptions, AccountSwitcher, PlayerSelect, TwoFactor, About, PatchNotes and FeedbackForm. The components never check credentials, and no secret reaches the log: LoginForm reports Submitted and the caller counts Failures; each new failure clears the password, refocuses it, shakes the form and names no field. TwoFactor builds on input.PinInput and drains an epoch-aligned period bar. PalettePicker draws each tile in its own palette on a pixel.Pad grid. pixel.Settings.Reduce stills motion like the system preference, the micro font gains '?', display.Face draws avatar faces anywhere, and input.PinInput pastes on Cmd+V as native hosts send it.
151 lines
3.6 KiB
Go
151 lines
3.6 KiB
Go
package pixel
|
||
|
||
import (
|
||
"fmt"
|
||
"log/slog"
|
||
"time"
|
||
|
||
"github.com/egoist/mygo/ui"
|
||
)
|
||
|
||
type Locale uint8
|
||
|
||
const (
|
||
En Locale = iota
|
||
Zh
|
||
)
|
||
|
||
type Settings struct {
|
||
Palette *Palette
|
||
// Dot is DIPs per dot, 1–4; zero means 2.
|
||
Dot int
|
||
Locale Locale
|
||
// Now replaces the frame clock, for tests.
|
||
Now func() time.Time
|
||
// Inert controls take no input, as under a busy veil.
|
||
Inert bool
|
||
// Reduce stills motion, as the system preference does.
|
||
Reduce bool
|
||
}
|
||
|
||
// Kit is one frame's settings, captured by components.
|
||
type Kit struct {
|
||
pal *Palette
|
||
dot float32
|
||
locale Locale
|
||
now time.Time
|
||
clock func() time.Time
|
||
reduce bool
|
||
}
|
||
|
||
func (k Kit) Palette() *Palette { return k.pal }
|
||
func (k Kit) C(s Slot) ui.Color { return k.pal.Colors[s] }
|
||
func (k Kit) Dot() float32 { return k.dot }
|
||
func (k Kit) D(n int) float32 { return float32(n) * k.dot }
|
||
func (k Kit) Locale() Locale { return k.locale }
|
||
func (k Kit) Now() time.Time { return k.now }
|
||
func (k Kit) Reduce() bool { return k.reduce }
|
||
|
||
type frame struct {
|
||
cur Settings
|
||
at time.Time
|
||
typing bool
|
||
// harbor holds the focus of controls turning off.
|
||
harbor *ui.Element
|
||
}
|
||
|
||
type frameKey struct{}
|
||
|
||
// Use must run once per frame, before any component.
|
||
func Use(c *ui.Context, s Settings) {
|
||
s = s.valid()
|
||
f := frameOf(c)
|
||
if f.cur.Palette != s.Palette {
|
||
slog.Debug("8bit: palette", "name", s.Palette.Name)
|
||
}
|
||
f.cur, f.at, f.typing, f.harbor = s, c.Now(), false, nil
|
||
c.SetTheme(theme(s.Palette))
|
||
}
|
||
|
||
// Typing reports a focused Editor built so far this frame.
|
||
func Typing(c *ui.Context) bool { return current(c).typing }
|
||
|
||
// Current returns the settings in force, for Scope to change.
|
||
func Current(c *ui.Context) Settings { return current(c).cur }
|
||
|
||
// Scope builds fn under other settings.
|
||
func Scope(c *ui.Context, s Settings, fn func()) {
|
||
f := current(c)
|
||
prev := f.cur
|
||
f.cur = s.valid()
|
||
fn()
|
||
f.cur = prev
|
||
}
|
||
|
||
// Lazy keeps this scope for rows MyGo builds during layout.
|
||
func Lazy[T any](c *ui.Context, fn func(T)) func(T) {
|
||
s := current(c).cur
|
||
return func(v T) { Scope(c, s, func() { fn(v) }) }
|
||
}
|
||
|
||
func K(c *ui.Context) Kit {
|
||
s := current(c).cur
|
||
now := c.Now()
|
||
if s.Now != nil {
|
||
now = s.Now()
|
||
}
|
||
return Kit{pal: s.Palette, dot: float32(s.Dot), locale: s.Locale, now: now, clock: s.Now, reduce: c.Preferences().ReduceMotion || s.Reduce}
|
||
}
|
||
|
||
func frameOf(c *ui.Context) *frame {
|
||
return ui.Local(c.Root(), frameKey{}, func() frame { return frame{} })
|
||
}
|
||
|
||
func current(c *ui.Context) *frame {
|
||
f := frameOf(c)
|
||
if !f.at.Equal(c.Now()) {
|
||
panic("pixel: Use was not called in this frame")
|
||
}
|
||
return f
|
||
}
|
||
|
||
func (s Settings) valid() Settings {
|
||
if s.Palette == nil {
|
||
panic("pixel: Settings.Palette is nil")
|
||
}
|
||
if s.Dot == 0 {
|
||
s.Dot = 2
|
||
}
|
||
if s.Dot < 1 || s.Dot > 4 {
|
||
panic(fmt.Sprintf("pixel: Settings.Dot %d outside 1–4", s.Dot))
|
||
}
|
||
if s.Locale > Zh {
|
||
panic(fmt.Sprintf("pixel: unknown Locale %d", s.Locale))
|
||
}
|
||
return s
|
||
}
|
||
|
||
// theme keeps MyGo's drawing in the palette; row highlights clear.
|
||
func theme(p *Palette) *ui.Theme {
|
||
t := ui.LightTheme()
|
||
c := p.Colors
|
||
t.Dark = p.Night
|
||
t.Background, t.Surface, t.SurfaceHover, t.SurfacePressed = c[Back], c[Face], ui.Transparent, c[Shade]
|
||
t.Border, t.Text, t.TextMuted = c[Edge], c[Ink], c[Faint]
|
||
t.Accent, t.AccentHover, t.AccentPressed, t.AccentText = ui.Transparent, c[AccentLit], c[Accent], c[AccentInk]
|
||
t.Danger, t.Warning, t.Success = c[Bad], c[Warn], c[Good]
|
||
t.Selection, t.Focus, t.Scrollbar, t.Radius = c[AccentLit], c[Accent], ui.Transparent, 0
|
||
return t
|
||
}
|
||
|
||
// Or is v, or def for zero; negatives panic.
|
||
func Or(name string, v, def int) int {
|
||
if v < 0 {
|
||
panic(fmt.Sprintf("pixel: %s %d is negative", name, v))
|
||
}
|
||
if v == 0 {
|
||
return def
|
||
}
|
||
return v
|
||
}
|