ui: decorate keypad grid for tactile calculator look
- Individual keys now use bordered mini-panels (NormalBorder 238) + dedicated bg/fg per type: - Numbers: neutral dark - Operators: 63 accent - Clears (C/AC): red-tinted warning - MOD: orange highlight - BASE: 63 border + bold (stands out) - Height(2) + padding for chunkier physical-button presence. - Pressed state inverts to flashStyle (bright 63 bg + white bold) for 140ms on every key action — direct 'click' feedback. - Gaps between keys + full-width backing panel (bg 234 + border) create a 'faceplate' effect so keys look like they sit on a real calculator keyboard. - Updated per-key tracking (pressedKey) in model + Update so flash is applied to the correct button. - Documented the new tactile decoration in docs/UI_DESIGN.md (replaced old plain-text description). - Matches the wide integrated display row from previous iteration. The keypad now feels much more tactile and calculator-like while staying true to the lipgloss + gostations patterns.
This commit is contained in:
parent
4f58ee9f2d
commit
b0dfc31767
@ -21,16 +21,25 @@ When a non-integer conversion is attempted (e.g. 3.833... + BASE / Tab), the num
|
||||
|
||||
This layout keeps the galculator spirit (base visible at a glance next to the big number) while being practical in a TUI and reducing vertical space. The entire row is one logical "large display".
|
||||
|
||||
## Keypad Grid
|
||||
## Keypad Grid (Tactile Decoration)
|
||||
Sparse for MVP (no memory, no %, no bitwise, no scientific, no A-F yet).
|
||||
|
||||
Uses the same button-cell rendering idiom as gostations `renderPlayback` / `makeButton`:
|
||||
The keypad is now decorated to evoke a physical, tactile calculator:
|
||||
|
||||
- `lipgloss.NewStyle().Width(5).Align(lipgloss.Center).Padding(0,1).Render(label)`
|
||||
- Active/pressed state uses green accent or the flashStyle (white on 63 bg, bold).
|
||||
- Flash on every key action using `tea.Tick` + clearFlashMsg (140ms), exactly as tuned in gostations player.
|
||||
- Each key is a bordered mini-panel (NormalBorder color 238) with its own background and foreground.
|
||||
- Base key style: dark bg 236, light fg 250, subtle border — gives a "raised key" appearance.
|
||||
- Specialized variants:
|
||||
- Number keys (0-9 .): neutral.
|
||||
- Operators (+ - * / =): accent color 63 for quick recognition.
|
||||
- Clears (C, AC): red-tinted bg 52 + fg 203 (warning/danger).
|
||||
- MOD: highlighted (214 orange).
|
||||
- BASE: prominent border color 63 + bold (the special display function).
|
||||
- Pressed state: the key inverts to the bright flashStyle (white fg on 63 bg, bold border) for ~140ms on every action. This provides immediate "tactile click" visual feedback (modeled directly on gostations volume/skip/stop flashes).
|
||||
- Keys use Height(2) for chunkier, more button-like presence.
|
||||
- Spacing (" " gaps) between keys and a backing panel (bg 234 + its own border) give the whole keypad a "faceplate" or "keyboard tray" look, making the individual keys pop as separate physical buttons.
|
||||
- The entire keypad backing is full-width (matching the display) and centered.
|
||||
|
||||
Layout sketch (subject to refinement during spike; 4-5 columns typical):
|
||||
Layout sketch (subject to refinement; 4-5 columns):
|
||||
|
||||
```
|
||||
7 8 9 / MOD
|
||||
@ -40,10 +49,12 @@ Layout sketch (subject to refinement during spike; 4-5 columns typical):
|
||||
BASE
|
||||
```
|
||||
|
||||
(Or tighter grouping. BASE is prominent because it is the distinctive "programmer display" feature.)
|
||||
BASE is prominent because it is the distinctive "programmer display" feature.
|
||||
|
||||
All important actions have direct keys (digits, operators, =/Enter, Backspace, Tab for BASE, c/C, etc.). The on-screen buttons are primarily visual + mouse targets.
|
||||
|
||||
This decoration keeps the UI lightweight while making the keypad feel far more "real" than plain text on background. Further polish (icons, better color themes, mouse hover if desired) can be added later.
|
||||
|
||||
## Overall Card & Polish
|
||||
- The entire calculator is wrapped in a **content-sized centered card**:
|
||||
- Outer rounded border (color 63, matching gostations label/border).
|
||||
|
||||
@ -28,8 +28,11 @@ type App struct {
|
||||
height int
|
||||
|
||||
// flash state for key actions and CERR
|
||||
flash string // "base", "cerr", "op", etc.
|
||||
flash string // "base", "cerr", "op", "key", etc.
|
||||
cerrFlash bool
|
||||
|
||||
// pressedKey tracks the last pressed keypad button for tactile "pressed" visual feedback
|
||||
pressedKey string
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
@ -57,6 +60,9 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if msg.which == "cerr" {
|
||||
a.cerrFlash = false
|
||||
}
|
||||
if msg.which == "key" {
|
||||
a.pressedKey = ""
|
||||
}
|
||||
if a.flash == msg.which {
|
||||
a.flash = ""
|
||||
}
|
||||
@ -74,41 +80,56 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return a, tick(600*time.Millisecond, "cerr")
|
||||
}
|
||||
a.flash = "base"
|
||||
return a, tick(140*time.Millisecond, "base")
|
||||
a.pressedKey = "BASE"
|
||||
return a, tick(140*time.Millisecond, "key")
|
||||
|
||||
case "c", "C":
|
||||
a.engine.ClearEntry()
|
||||
a.flash = "clear"
|
||||
return a, tick(140*time.Millisecond, "clear")
|
||||
a.pressedKey = "C"
|
||||
return a, tick(140*time.Millisecond, "key")
|
||||
|
||||
case "backspace":
|
||||
a.engine.Backspace()
|
||||
|
||||
case ".":
|
||||
a.engine.EnterDecimalPoint()
|
||||
a.pressedKey = "."
|
||||
return a, tick(140*time.Millisecond, "key")
|
||||
|
||||
case "+", "-", "*", "/":
|
||||
a.engine.SetOperator(msg.String())
|
||||
a.flash = "op"
|
||||
return a, tick(140*time.Millisecond, "op")
|
||||
a.pressedKey = msg.String()
|
||||
return a, tick(140*time.Millisecond, "key")
|
||||
|
||||
case "m", "M": // MOD as immediate for convenience in spike
|
||||
a.engine.Mod()
|
||||
a.flash = "op"
|
||||
return a, tick(140*time.Millisecond, "op")
|
||||
a.pressedKey = "MOD"
|
||||
return a, tick(140*time.Millisecond, "key")
|
||||
|
||||
case "=", "enter":
|
||||
a.engine.Equals()
|
||||
a.flash = "eq"
|
||||
return a, tick(140*time.Millisecond, "eq")
|
||||
a.pressedKey = "="
|
||||
return a, tick(140*time.Millisecond, "key")
|
||||
|
||||
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
|
||||
a.engine.EnterDigit(rune(msg.String()[0]))
|
||||
a.pressedKey = msg.String()
|
||||
return a, tick(140*time.Millisecond, "key")
|
||||
|
||||
case "ac", "ctrl+l": // All Clear
|
||||
a.engine.AllClear()
|
||||
a.flash = "clear"
|
||||
return a, tick(140*time.Millisecond, "clear")
|
||||
a.pressedKey = "AC"
|
||||
return a, tick(140*time.Millisecond, "key")
|
||||
|
||||
case "+/-":
|
||||
a.engine.ChangeSign()
|
||||
a.pressedKey = "+/-"
|
||||
return a, tick(140*time.Millisecond, "key")
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
@ -178,24 +199,77 @@ func (a *App) View() string {
|
||||
|
||||
display := lcd.Render(displayRow)
|
||||
|
||||
// Minimal keypad grid (text cells for the spike; real button cells can be expanded)
|
||||
btn := func(s string) string {
|
||||
st := lipgloss.NewStyle().Width(5).Align(lipgloss.Center).Padding(0, 1)
|
||||
if a.flash != "" && s == "BASE" {
|
||||
return flashStyle.Width(5).Align(lipgloss.Center).Padding(0, 1).Render(s)
|
||||
// --- Tactile keypad decoration ---
|
||||
// Base style for a "key": bordered, slightly raised look with dark background.
|
||||
// This gives each button a physical, tactile calculator key appearance.
|
||||
keyStyle := lipgloss.NewStyle().
|
||||
Width(5).
|
||||
Height(2).
|
||||
Align(lipgloss.Center).
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("238")).
|
||||
Background(lipgloss.Color("236")).
|
||||
Foreground(lipgloss.Color("250"))
|
||||
|
||||
// Pressed/activated style: bright inversion for that "key is being pressed" feel.
|
||||
pressedStyle := flashStyle.
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("63")).
|
||||
Width(5).
|
||||
Height(2).
|
||||
Align(lipgloss.Center)
|
||||
|
||||
// Specialized key styles for visual grouping (like real calculators).
|
||||
numKey := keyStyle.Copy()
|
||||
opKey := keyStyle.Copy().Foreground(lipgloss.Color("63")).Background(lipgloss.Color("235"))
|
||||
clearKey := keyStyle.Copy().Foreground(lipgloss.Color("203")).Background(lipgloss.Color("52"))
|
||||
modKey := keyStyle.Copy().Foreground(lipgloss.Color("214")) // orange-ish for MOD
|
||||
baseKey := keyStyle.Copy().BorderForeground(lipgloss.Color("63")).Bold(true)
|
||||
|
||||
makeKey := func(label string) string {
|
||||
var st lipgloss.Style
|
||||
switch label {
|
||||
case "+", "-", "*", "/", "=":
|
||||
st = opKey
|
||||
case "C", "AC":
|
||||
st = clearKey
|
||||
case "MOD":
|
||||
st = modKey
|
||||
case "BASE":
|
||||
st = baseKey
|
||||
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".":
|
||||
st = numKey
|
||||
case "+/-":
|
||||
st = opKey
|
||||
default:
|
||||
st = keyStyle
|
||||
}
|
||||
return st.Render(s)
|
||||
if a.pressedKey == label {
|
||||
st = pressedStyle
|
||||
}
|
||||
return st.Render(label)
|
||||
}
|
||||
|
||||
row1 := lipgloss.JoinHorizontal(lipgloss.Top, btn("7"), btn("8"), btn("9"), btn("/"), btn("MOD"))
|
||||
row2 := lipgloss.JoinHorizontal(lipgloss.Top, btn("4"), btn("5"), btn("6"), btn("*"), btn("C"))
|
||||
row3 := lipgloss.JoinHorizontal(lipgloss.Top, btn("1"), btn("2"), btn("3"), btn("-"), btn("AC"))
|
||||
row4 := lipgloss.JoinHorizontal(lipgloss.Top, btn("0"), btn("."), btn("+/-"), btn("+"), btn("="))
|
||||
rowBase := lipgloss.JoinHorizontal(lipgloss.Top, " ", btn("BASE"), " ")
|
||||
spacer := " " // gap between keys for tactile separation
|
||||
row1 := lipgloss.JoinHorizontal(lipgloss.Top, makeKey("7"), spacer, makeKey("8"), spacer, makeKey("9"), spacer, makeKey("/"), spacer, makeKey("MOD"))
|
||||
row2 := lipgloss.JoinHorizontal(lipgloss.Top, makeKey("4"), spacer, makeKey("5"), spacer, makeKey("6"), spacer, makeKey("*"), spacer, makeKey("C"))
|
||||
row3 := lipgloss.JoinHorizontal(lipgloss.Top, makeKey("1"), spacer, makeKey("2"), spacer, makeKey("3"), spacer, makeKey("-"), spacer, makeKey("AC"))
|
||||
row4 := lipgloss.JoinHorizontal(lipgloss.Top, makeKey("0"), spacer, makeKey("."), spacer, makeKey("+/-"), spacer, makeKey("+"), spacer, makeKey("="))
|
||||
rowBase := lipgloss.JoinHorizontal(lipgloss.Top, makeKey("BASE"))
|
||||
|
||||
// Compact grid of keys
|
||||
rawGrid := lipgloss.JoinVertical(lipgloss.Left, row1, row2, row3, row4, rowBase)
|
||||
// Center the keypad grid under the (now wide) display
|
||||
grid := lipgloss.NewStyle().Width(dispW).Align(lipgloss.Center).Render(rawGrid)
|
||||
|
||||
// Keypad backing panel: gives the whole grid a "calculator faceplate" look.
|
||||
// Darker background, subtle border, padding. Centered under the wide display.
|
||||
keypad := lipgloss.NewStyle().
|
||||
Border(lipgloss.NormalBorder()).
|
||||
BorderForeground(lipgloss.Color("238")).
|
||||
Background(lipgloss.Color("234")).
|
||||
Padding(1, 2).
|
||||
Width(dispW).
|
||||
Align(lipgloss.Center).
|
||||
Render(rawGrid)
|
||||
|
||||
// Hint (minimal, non-wrapping, like gostations final player)
|
||||
hint := lipgloss.NewStyle().Faint(true).Render("Tab:BASE 0-9 . = + - * / m:MOD c:C ac:AC q:quit")
|
||||
@ -206,7 +280,7 @@ func (a *App) View() string {
|
||||
"",
|
||||
display,
|
||||
"",
|
||||
grid,
|
||||
keypad,
|
||||
"",
|
||||
centeredHint,
|
||||
)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user