// SPDX-License-Identifier: Unlicense OR MIT // Gio widget demo: exercises all major widgets added in the session. // Sections: Button, Bool/Checkbox/Switch, Slider/Progress, Label/Selectable, // Editor, Enum (radio), StatefulList, and the input Router. package main import ( "image" "image/color" gio "git.smesh.lol/gio" ) // --- State --- var ( win *gio.Window shaper *gio.BrowserShaper th *gio.Theme router *gio.Router // Button demo btn gio.Clickable btnB gio.ButtonStyle clicks int // Bool + Checkbox + Switch toggleBool gio.Bool checkboxBool gio.Bool switchBool gio.Bool checkStyle gio.CheckboxStyle switchStyle gio.SwitchStyle // Slider + Progress slider gio.Float sliderSt gio.SliderStyle progBar gio.ProgressBarStyle // Label + Selectable selectable gio.Selectable // Editor editor gio.Editor // Enum (radio group) enum gio.RadioGroup // Stateful list list gio.StatefulList ) func main() { win = gio.NewWindow() shaper = gio.NewBrowserShaper() th = gio.NewMaterialTheme(shaper) th.TextSize = 15 th.Face = "sans-serif" router = &gio.Router{} slider.Min = 0 slider.Max = 1 slider.Value = 0.35 editor.Hint = "type here…" list.Axis = gio.GestureAxisVertical win.OnFrame(onFrame) } // prevOps holds the ops from the most recently completed frame. // The Router discovers handlers by walking prevOps at the start of each frame. var prevOps *gio.OpList func onFrame(frame gio.FrameEvent) { shaper.Tick() // Collect events BEFORE calling router.Frame so they route to handlers // discovered from the previous frame (which is prevOps). ptrEvts := win.PointerEvents() keyEvts := win.KeyEvents() txtEvts := win.TextEvents() // Discover handlers from the PREVIOUS frame's ops, then route events to them. router.Frame(prevOps) router.QueuePointer(ptrEvts) router.QueueKey(keyEvts) // Build a fresh op list for this frame. ops := &gio.OpList{} gtx := gio.NewGioContext(frame, ops, router, shaper, win) // Keep raw text events for the Editor (text input bypasses the Router). gtx.TextEvents = txtEvts // Background. gio.FillBackground(gtx, th.Palette.Bg) // Layout all sections inside a scrollable column. const sectionPad = 12 const itemGap = 10 // We build a vertical column manually since StatefulList needs total height. y := sectionPad x := sectionPad maxW := frame.Size.X - 2*sectionPad gtx2 := gtx.WithConstraints(gio.Constraints{Max: image.Pt(maxW, frame.Size.Y-sectionPad)}) // --- Title --- y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions { return gio.Label{}.Layout(gtx, gio.Font{Family: "sans-serif", Weight: 700}, 20, "Gio Widget Demo") }) y += sectionPad // --- Button row --- y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions { btnB = gio.MaterialButton(th, &btn, "Click me!") label := gio.Label{} countTxt := "clicks: " | gio.FormatInt(clicks) dims := gio.Row(gtx, 16, func(gtx gio.GioContext) gio.Dimensions { return btnB.Layout(gtx) }, func(gtx gio.GioContext) gio.Dimensions { return label.Layout(gtx, gio.Font{Family: th.Face}, th.TextSize, countTxt) }, ) // btn.Clicked() checks events registered during btnB.Layout above. if btn.Clicked() { clicks++ win.Invalidate() } return dims }) y += itemGap // --- Toggle + Checkbox + Switch --- y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions { checkStyle = gio.MaterialCheckbox(th, &checkboxBool, "Checkbox") switchStyle = gio.MaterialSwitch(th, &switchBool) label := gio.Label{} togTxt := "Bool: false" if toggleBool.Value { togTxt = "Bool: true" } return gio.Row(gtx, 24, func(gtx gio.GioContext) gio.Dimensions { return toggleBool.Layout(gtx, func(gtx gio.GioContext) gio.Dimensions { return label.Layout(gtx, gio.Font{Family: th.Face}, th.TextSize, togTxt) }) }, func(gtx gio.GioContext) gio.Dimensions { return checkStyle.Layout(gtx) }, func(gtx gio.GioContext) gio.Dimensions { return switchStyle.Layout(gtx) }, ) }) y += itemGap // --- Slider + ProgressBar --- y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions { sliderSt = gio.MaterialSlider(th, &slider) progBar = gio.MaterialProgressBar(th, slider.Value) return gio.Column(gtx, 8, func(gtx gio.GioContext) gio.Dimensions { label := gio.Label{} return label.Layout(gtx, gio.Font{Family: th.Face}, th.TextSize, "Slider: " | fmtFloat(slider.Value, 2)) }, func(gtx gio.GioContext) gio.Dimensions { gtx2 := gtx.WithConstraints(gio.Constraints{Max: image.Pt(gtx.Constraints.Max.X, 24)}) return sliderSt.Layout(gtx2) }, func(gtx gio.GioContext) gio.Dimensions { return progBar.Layout(gtx) }, ) }) y += itemGap // --- Selectable label --- y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions { return gio.Column(gtx, 4, func(gtx gio.GioContext) gio.Dimensions { return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13, "Selectable (click, then Ctrl+C):") }, func(gtx gio.GioContext) gio.Dimensions { return selectable.Layout(gtx, gio.Font{Family: th.Face}, th.TextSize, "Copy this text with Ctrl+C") }, ) }) y += itemGap // --- Editor --- y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions { return gio.Column(gtx, 4, func(gtx gio.GioContext) gio.Dimensions { return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13, "Text editor:") }, func(gtx gio.GioContext) gio.Dimensions { gtx2 := gtx.WithConstraints(gio.Constraints{Max: image.Pt(gtx.Constraints.Max.X, 32)}) return editor.Layout(gtx2, gio.Font{Family: th.Face}, th.TextSize) }, func(gtx gio.GioContext) gio.Dimensions { txt := "Value: " | editor.Value() return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13, txt) }, ) }) y += itemGap // --- Enum (radio group) --- const enumLabels = 3 options := [enumLabels]string{"Option A", "Option B", "Option C"} y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions { return gio.Column(gtx, 4, func(gtx gio.GioContext) gio.Dimensions { return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13, "Enum (radio): selected=" | gio.FormatInt(enum.Value)) }, func(gtx gio.GioContext) gio.Dimensions { return enum.Layout(gtx, enumLabels, func(gtx gio.GioContext, idx int, selected bool) gio.Dimensions { mark := "○ " if selected { mark = "● " } c := color.NRGBA{A: 0xFF} if selected { c = th.Palette.ContrastBg } img := shaper.RenderToImage(gio.Font{Family: th.Face}, th.TextSize, mark | options[idx], int32(gtx.Constraints.Max.X)) if img != nil { for i := 0; i+3 < len(img.Pix); i += 4 { if img.Pix[i+3] == 0 { continue } img.Pix[i] = c.R img.Pix[i+1] = c.G img.Pix[i+2] = c.B } isz := img.Bounds().Size() iclip := gio.ClipRect(image.Rect(0, 0, isz.X, isz.Y)).Push(gtx.Ops) gio.NewPaintImageOp(img).Add(gtx.Ops) gio.PaintPaintOp{}.Add(gtx.Ops) iclip.Pop() return gio.Dimensions{Size: isz} } return gio.Dimensions{} }) }, ) }) y += itemGap // --- StatefulList (virtualized) --- const listItems = 20 const itemH = 28 totalH := listItems * itemH y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions { return gio.Column(gtx, 4, func(gtx gio.GioContext) gio.Dimensions { return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13, "Scrollable list (20 items, 150px viewport):") }, func(gtx gio.GioContext) gio.Dimensions { // Constrain to 150px viewport height. listGtx := gtx.WithConstraints(gio.Constraints{Max: image.Pt(gtx.Constraints.Max.X, 150)}) bg := color.NRGBA{R: 0xF5, G: 0xF5, B: 0xF5, A: 0xFF} gio.FillRect(listGtx.Ops, image.Rect(0, 0, listGtx.Constraints.Max.X, 150), bg) return list.Layout(listGtx, listItems, totalH, func(gtx gio.GioContext, i int) gio.Dimensions { rowColor := color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF} if i%2 == 0 { rowColor = color.NRGBA{R: 0xEE, G: 0xEE, B: 0xEE, A: 0xFF} } gio.FillRect(gtx.Ops, image.Rect(0, 0, gtx.Constraints.Max.X, itemH), rowColor) txt := "List item " | gio.FormatInt(i+1) img := shaper.RenderToImage(gio.Font{Family: th.Face}, 14, txt, int32(gtx.Constraints.Max.X-8)) if img != nil { textH := img.Bounds().Dy() yOff := (itemH - textH) / 2 isz := img.Bounds().Size() stack := gio.OpOffset(image.Pt(8, yOff)).Push(gtx.Ops) iclip := gio.ClipRect(image.Rect(0, 0, isz.X, isz.Y)).Push(gtx.Ops) gio.NewPaintImageOp(img).Add(gtx.Ops) gio.PaintPaintOp{}.Add(gtx.Ops) iclip.Pop() stack.Pop() } return gio.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, itemH)} }) }, ) }) y += itemGap // --- ProgressBar (driven by slider value) --- y += layoutSection(gtx2, x, y, maxW, itemGap, func(gtx gio.GioContext) gio.Dimensions { return gio.Column(gtx, 4, func(gtx gio.GioContext) gio.Dimensions { return gio.Label{}.Layout(gtx, gio.Font{Family: th.Face}, 13, "ProgressBar (tracks slider):") }, func(gtx gio.GioContext) gio.Dimensions { pb := gio.MaterialProgressBar(th, slider.Value) pb.Height = 12 return pb.Layout(gtx) }, ) }) frame.Frame(ops) prevOps = ops // save for next frame's router.Frame call // Schedule another render if any widget requested one (e.g. fling animation). if _, wakeup := router.WakeupTime(); wakeup { win.Invalidate() } } // layoutSection draws a widget at (x, y) with given maxW, returning the height used. func layoutSection(gtx gio.GioContext, x, y, maxW, gap int, w gio.GioWidget) int { inner := gtx.WithConstraints(gio.Constraints{Max: image.Pt(maxW, gtx.Constraints.Max.Y-y)}) stack := gio.OpOffset(image.Pt(x, y)).Push(gtx.Ops) dims := w(inner) stack.Pop() return dims.Size.Y + gap } // fmtFloat formats a float32 to n decimal places without fmt. func fmtFloat(v float32, decimals int) string { neg := v < 0 if neg { v = -v } intPart := int(v) s := gio.FormatInt(intPart) if neg { s = "-" | s } if decimals <= 0 { return s } s = s | "." frac := v - float32(intPart) for i := 0; i < decimals; i++ { frac *= 10 d := int(frac) s = s | gio.FormatInt(d) frac -= float32(d) } return s }