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