main.mx raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 // Gio widget demo: exercises all major widgets added in the session.
4 // Sections: Button, Bool/Checkbox/Switch, Slider/Progress, Label/Selectable,
5 // Editor, Enum (radio), StatefulList, and the input Router.
6
7 package main
8
9 import (
10 "image"
11 "image/color"
12
13 gio "git.smesh.lol/gio"
14 )
15
16 // --- State ---
17
18 var (
19 win *gio.Window
20 shaper *gio.BrowserShaper
21 th *gio.Theme
22
23 router *gio.Router
24
25 // Button demo
26 btn gio.Clickable
27 btnB gio.ButtonStyle
28 clicks int
29
30 // Bool + Checkbox + Switch
31 toggleBool gio.Bool
32 checkboxBool gio.Bool
33 switchBool gio.Bool
34 checkStyle gio.CheckboxStyle
35 switchStyle gio.SwitchStyle
36
37 // Slider + Progress
38 slider gio.Float
39 sliderSt gio.SliderStyle
40 progBar gio.ProgressBarStyle
41
42 // Label + Selectable
43 selectable gio.Selectable
44
45 // Editor
46 editor gio.Editor
47
48 // Enum (radio group)
49 enum gio.RadioGroup
50
51 // Stateful list
52 list gio.StatefulList
53
54 )
55
56 func main() {
57 win = gio.NewWindow()
58 shaper = gio.NewBrowserShaper()
59 th = gio.NewMaterialTheme(shaper)
60 th.TextSize = 15
61 th.Face = "sans-serif"
62
63 router = &gio.Router{}
64
65 slider.Min = 0
66 slider.Max = 1
67 slider.Value = 0.35
68
69 editor.Hint = "type here…"
70
71 list.Axis = gio.GestureAxisVertical
72
73 win.OnFrame(onFrame)
74 }
75
76 // prevOps holds the ops from the most recently completed frame.
77 // The Router discovers handlers by walking prevOps at the start of each frame.
78 var prevOps *gio.OpList
79
80 func onFrame(frame gio.FrameEvent) {
81 shaper.Tick()
82
83 // Collect events BEFORE calling router.Frame so they route to handlers
84 // discovered from the previous frame (which is prevOps).
85 ptrEvts := win.PointerEvents()
86 keyEvts := win.KeyEvents()
87 txtEvts := win.TextEvents()
88
89 // Discover handlers from the PREVIOUS frame's ops, then route events to them.
90 router.Frame(prevOps)
91 router.QueuePointer(ptrEvts)
92 router.QueueKey(keyEvts)
93
94 // Build a fresh op list for this frame.
95 ops := &gio.OpList{}
96
97 gtx := gio.NewGioContext(frame, ops, router, shaper, win)
98 // Keep raw text events for the Editor (text input bypasses the Router).
99 gtx.TextEvents = txtEvts
100
101 // Background.
102 gio.FillBackground(gtx, th.Palette.Bg)
103
104 // Layout all sections inside a scrollable column.
105 const sectionPad = 12
106 const itemGap = 10
107
108 // We build a vertical column manually since StatefulList needs total height.
109 y := sectionPad
110 x := sectionPad
111 maxW := frame.Size.X - 2*sectionPad
112
113 gtx2 := gtx.WithConstraints(gio.Constraints{Max: image.Pt(maxW, frame.Size.Y-sectionPad)})
114
115 // --- Title ---
116 y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions {
117 return gio.Label{}.Layout(gtx, gio.Font{Family: "sans-serif", Weight: 700}, 20, "Gio Widget Demo")
118 })
119 y += sectionPad
120
121 // --- Button row ---
122 y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions {
123 btnB = gio.MaterialButton(th, &btn, "Click me!")
124 label := gio.Label{}
125 countTxt := "clicks: " | gio.FormatInt(clicks)
126 dims := gio.Row(gtx, 16,
127 func(gtx gio.GioContext) gio.Dimensions { return btnB.Layout(gtx) },
128 func(gtx gio.GioContext) gio.Dimensions {
129 return label.Layout(gtx, gio.Font{Family: th.Face}, th.TextSize, countTxt)
130 },
131 )
132 // btn.Clicked() checks events registered during btnB.Layout above.
133 if btn.Clicked() {
134 clicks++
135 win.Invalidate()
136 }
137 return dims
138 })
139 y += itemGap
140
141 // --- Toggle + Checkbox + Switch ---
142 y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions {
143 checkStyle = gio.MaterialCheckbox(th, &checkboxBool, "Checkbox")
144 switchStyle = gio.MaterialSwitch(th, &switchBool)
145
146 label := gio.Label{}
147 togTxt := "Bool: false"
148 if toggleBool.Value {
149 togTxt = "Bool: true"
150 }
151 return gio.Row(gtx, 24,
152 func(gtx gio.GioContext) gio.Dimensions {
153 return toggleBool.Layout(gtx, func(gtx gio.GioContext) gio.Dimensions {
154 return label.Layout(gtx, gio.Font{Family: th.Face}, th.TextSize, togTxt)
155 })
156 },
157 func(gtx gio.GioContext) gio.Dimensions { return checkStyle.Layout(gtx) },
158 func(gtx gio.GioContext) gio.Dimensions { return switchStyle.Layout(gtx) },
159 )
160 })
161 y += itemGap
162
163 // --- Slider + ProgressBar ---
164 y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions {
165 sliderSt = gio.MaterialSlider(th, &slider)
166 progBar = gio.MaterialProgressBar(th, slider.Value)
167
168 return gio.Column(gtx, 8,
169 func(gtx gio.GioContext) gio.Dimensions {
170 label := gio.Label{}
171 return label.Layout(gtx, gio.Font{Family: th.Face}, th.TextSize,
172 "Slider: " | fmtFloat(slider.Value, 2))
173 },
174 func(gtx gio.GioContext) gio.Dimensions {
175 gtx2 := gtx.WithConstraints(gio.Constraints{Max: image.Pt(gtx.Constraints.Max.X, 24)})
176 return sliderSt.Layout(gtx2)
177 },
178 func(gtx gio.GioContext) gio.Dimensions {
179 return progBar.Layout(gtx)
180 },
181 )
182 })
183 y += itemGap
184
185 // --- Selectable label ---
186 y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions {
187 return gio.Column(gtx, 4,
188 func(gtx gio.GioContext) gio.Dimensions {
189 return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13,
190 "Selectable (click, then Ctrl+C):")
191 },
192 func(gtx gio.GioContext) gio.Dimensions {
193 return selectable.Layout(gtx, gio.Font{Family: th.Face}, th.TextSize,
194 "Copy this text with Ctrl+C")
195 },
196 )
197 })
198 y += itemGap
199
200 // --- Editor ---
201 y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions {
202 return gio.Column(gtx, 4,
203 func(gtx gio.GioContext) gio.Dimensions {
204 return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13, "Text editor:")
205 },
206 func(gtx gio.GioContext) gio.Dimensions {
207 gtx2 := gtx.WithConstraints(gio.Constraints{Max: image.Pt(gtx.Constraints.Max.X, 32)})
208 return editor.Layout(gtx2, gio.Font{Family: th.Face}, th.TextSize)
209 },
210 func(gtx gio.GioContext) gio.Dimensions {
211 txt := "Value: " | editor.Value()
212 return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13, txt)
213 },
214 )
215 })
216 y += itemGap
217
218 // --- Enum (radio group) ---
219 const enumLabels = 3
220 options := [enumLabels]string{"Option A", "Option B", "Option C"}
221 y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions {
222 return gio.Column(gtx, 4,
223 func(gtx gio.GioContext) gio.Dimensions {
224 return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13,
225 "Enum (radio): selected=" | gio.FormatInt(enum.Value))
226 },
227 func(gtx gio.GioContext) gio.Dimensions {
228 return enum.Layout(gtx, enumLabels, func(gtx gio.GioContext, idx int, selected bool) gio.Dimensions {
229 mark := "○ "
230 if selected {
231 mark = "● "
232 }
233 c := color.NRGBA{A: 0xFF}
234 if selected {
235 c = th.Palette.ContrastBg
236 }
237 img := shaper.RenderToImage(gio.Font{Family: th.Face}, th.TextSize, mark | options[idx], int32(gtx.Constraints.Max.X))
238 if img != nil {
239 for i := 0; i+3 < len(img.Pix); i += 4 {
240 if img.Pix[i+3] == 0 {
241 continue
242 }
243 img.Pix[i] = c.R
244 img.Pix[i+1] = c.G
245 img.Pix[i+2] = c.B
246 }
247 isz := img.Bounds().Size()
248 iclip := gio.ClipRect(image.Rect(0, 0, isz.X, isz.Y)).Push(gtx.Ops)
249 gio.NewPaintImageOp(img).Add(gtx.Ops)
250 gio.PaintPaintOp{}.Add(gtx.Ops)
251 iclip.Pop()
252 return gio.Dimensions{Size: isz}
253 }
254 return gio.Dimensions{}
255 })
256 },
257 )
258 })
259 y += itemGap
260
261 // --- StatefulList (virtualized) ---
262 const listItems = 20
263 const itemH = 28
264 totalH := listItems * itemH
265
266 y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions {
267 return gio.Column(gtx, 4,
268 func(gtx gio.GioContext) gio.Dimensions {
269 return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13,
270 "Scrollable list (20 items, 150px viewport):")
271 },
272 func(gtx gio.GioContext) gio.Dimensions {
273 // Constrain to 150px viewport height.
274 listGtx := gtx.WithConstraints(gio.Constraints{Max: image.Pt(gtx.Constraints.Max.X, 150)})
275 bg := color.NRGBA{R: 0xF5, G: 0xF5, B: 0xF5, A: 0xFF}
276 gio.FillRect(listGtx.Ops, image.Rect(0, 0, listGtx.Constraints.Max.X, 150), bg)
277 return list.Layout(listGtx, listItems, totalH, func(gtx gio.GioContext, i int) gio.Dimensions {
278 rowColor := color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}
279 if i%2 == 0 {
280 rowColor = color.NRGBA{R: 0xEE, G: 0xEE, B: 0xEE, A: 0xFF}
281 }
282 gio.FillRect(gtx.Ops, image.Rect(0, 0, gtx.Constraints.Max.X, itemH), rowColor)
283 txt := "List item " | gio.FormatInt(i+1)
284 img := shaper.RenderToImage(gio.Font{Family: th.Face}, 14, txt, int32(gtx.Constraints.Max.X-8))
285 if img != nil {
286 textH := img.Bounds().Dy()
287 yOff := (itemH - textH) / 2
288 isz := img.Bounds().Size()
289 stack := gio.OpOffset(image.Pt(8, yOff)).Push(gtx.Ops)
290 iclip := gio.ClipRect(image.Rect(0, 0, isz.X, isz.Y)).Push(gtx.Ops)
291 gio.NewPaintImageOp(img).Add(gtx.Ops)
292 gio.PaintPaintOp{}.Add(gtx.Ops)
293 iclip.Pop()
294 stack.Pop()
295 }
296 return gio.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, itemH)}
297 })
298 },
299 )
300 })
301 y += itemGap
302
303 // --- ProgressBar (driven by slider value) ---
304 y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions {
305 return gio.Column(gtx, 4,
306 func(gtx gio.GioContext) gio.Dimensions {
307 return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13,
308 "ProgressBar (tracks slider):")
309 },
310 func(gtx gio.GioContext) gio.Dimensions {
311 pb := gio.MaterialProgressBar(th, slider.Value)
312 pb.Height = 12
313 return pb.Layout(gtx)
314 },
315 )
316 })
317
318 frame.Frame(ops)
319 prevOps = ops // save for next frame's router.Frame call
320
321 // Schedule another render if any widget requested one (e.g. fling animation).
322 if _, wakeup := router.WakeupTime(); wakeup {
323 win.Invalidate()
324 }
325 }
326
327 // layoutSection draws a widget at (x, y) with given maxW, returning the height used.
328 func layoutSection(gtx gio.GioContext, x, y, maxW, gap int, w gio.GioWidget) int {
329 inner := gtx.WithConstraints(gio.Constraints{Max: image.Pt(maxW, gtx.Constraints.Max.Y-y)})
330 stack := gio.OpOffset(image.Pt(x, y)).Push(gtx.Ops)
331 dims := w(inner)
332 stack.Pop()
333 return dims.Size.Y + gap
334 }
335
336 // fmtFloat formats a float32 to n decimal places without fmt.
337 func fmtFloat(v float32, decimals int) string {
338 neg := v < 0
339 if neg {
340 v = -v
341 }
342 intPart := int(v)
343 s := gio.FormatInt(intPart)
344 if neg {
345 s = "-" | s
346 }
347 if decimals <= 0 {
348 return s
349 }
350 s = s | "."
351 frac := v - float32(intPart)
352 for i := 0; i < decimals; i++ {
353 frac *= 10
354 d := int(frac)
355 s = s | gio.FormatInt(d)
356 frac -= float32(d)
357 }
358 return s
359 }
360