-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.gala
More file actions
121 lines (108 loc) · 4.29 KB
/
main.gala
File metadata and controls
121 lines (108 loc) · 4.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package main
import (
. "github.com/martianoff/gala-tui"
. "martianoff/gala/collection_immutable"
. "martianoff/gala/std"
)
// ============================================================================
// custom_widget — author your own widget with its own event vocabulary.
//
// gala build ./examples/custom_widget
// ./gala_tui.exe # binary takes the module name
//
// We define a `RatingView` widget — a horizontal row of star symbols —
// that exposes a single `OnSet(score)` event. Clicking a star sets the
// rating. Keyboard arrows + Enter also work. Both inputs share the same
// model field, so the rating reflects whichever path the user took.
//
// The takeaway: a user-authored widget defines its own constructor that
// takes typed callbacks and attaches them to interactive sub-regions
// with the fluent `.OnClick(msg)` method. Call sites read the widget
// like a built-in.
//
// All Msg cases use the `Cw` prefix because dot-importing gala-tui pulls
// in sealed-type case constructors (`NoOp`, `Quit`, `Up`, `Down`, …)
// that would otherwise shadow generic names.
// ============================================================================
struct Model(Score int, Max int)
sealed type Msg {
case CwSet(Score int) // rating changed (set absolute score)
case CwLeft() // arrow-left → bump score down
case CwRight() // arrow-right → bump score up
case CwQuit()
case CwNoOp()
}
// ----- The user-authored widget --------------------------------------------
// RatingView renders `r.Score` filled stars followed by `r.Max - r.Score`
// empty stars. Each star fires `OnSet(starIndex + 1)` on click — i.e.,
// clicking the third star sets Score to 3.
//
// The constructor's `OnSet` parameter is the entire event contract.
// Each cell uses the fluent `.OnClick(msg)` method to attach its click
// payload — no wrapper widgets visible in the call shape.
func RatingView[T any](
r Model,
OnSet func(int) T,
) Widget {
val cells = ArrayTabulate(r.Max, (i) => {
val star = i + 1 // 1-based score
val glyph = if (star <= r.Score) "★" else "☆"
val style = if (star <= r.Score)
DefaultStyle().WithBold().WithFg(BrightYellow())
else
DefaultStyle().WithDim()
val cell = TextStyled(" " + glyph + " ", style).OnClick(OnSet(star))
return Fixed(3, cell)
})
return Row(cells)
}
// ----- Plumbing ------------------------------------------------------------
func update(m Model, msg Msg) Tuple[Model, Cmd[Msg]] = msg match {
case CwQuit() => (m, QuitCmd[Msg]())
case CwNoOp() => (m, NoCmd[Msg]())
case CwSet(s) => (m.Copy(Score = clamp(s, 0, m.Max)), NoCmd[Msg]())
case CwLeft() => (m.Copy(Score = clamp(m.Score - 1, 0, m.Max)), NoCmd[Msg]())
case CwRight() => (m.Copy(Score = clamp(m.Score + 1, 0, m.Max)), NoCmd[Msg]())
}
func clamp(v int, lo int, hi int) int {
if v < lo { return lo }
if v > hi { return hi }
return v
}
func view(m Model) Widget {
val title = TextStyled(" Rate this thing (←/→ to adjust, or click a star) ",
DefaultStyle().WithBold())
// Use the user-authored widget exactly like a built-in:
val rating = RatingView[Msg](m, (s) => CwSet(Score = s))
val footer = TextStyled(s" Score: ${m.Score} / ${m.Max} (q to quit)",
DefaultStyle().WithDim())
return Column(ArrayOf[LayoutChild](
Fixed(1, title),
Fixed(1, Text("")),
Fixed(1, rating),
Fixed(1, Text("")),
Fixed(1, footer),
))
}
func keyToMsg(ev KeyEvent) Msg {
if KeyMatchesAny(ev, "ctrl+c", "q") { return CwQuit() }
return ev.Key match {
case ArrowLeft() => CwLeft()
case ArrowRight() => CwRight()
case _ => CwNoOp()
}
}
// inputToMsg only sees non-click events — the framework dispatches
// left-clicks against the registry before this function runs.
func inputToMsg(ev InputEvent) Msg = ev match {
case KeyInput(k) => keyToMsg(k)
case MouseInput(_) => CwNoOp()
case _ => CwNoOp()
}
func main() {
val _ = RunSimpleWithMouse[Model, Msg](
Model(Score = 3, Max = 5),
update, view, inputToMsg,
NoSubs[Msg](),
)
}