gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/widget/material/switch.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package material 4 5 import ( 6 "image" 7 "image/color" 8 9 "gioui.org/internal/f32color" 10 "gioui.org/io/semantic" 11 "gioui.org/layout" 12 "gioui.org/op" 13 "gioui.org/op/clip" 14 "gioui.org/op/paint" 15 "gioui.org/widget" 16 ) 17 18 type SwitchStyle struct { 19 Description string 20 Color struct { 21 Enabled color.NRGBA 22 Disabled color.NRGBA 23 Track color.NRGBA 24 } 25 Switch *widget.Bool 26 } 27 28 // Switch is for selecting a boolean value. 29 func Switch(th *Theme, swtch *widget.Bool, description string) SwitchStyle { 30 sw := SwitchStyle{ 31 Switch: swtch, 32 Description: description, 33 } 34 sw.Color.Enabled = th.Palette.ContrastBg 35 sw.Color.Disabled = th.Palette.Bg 36 sw.Color.Track = f32color.MulAlpha(th.Palette.Fg, 0x88) 37 return sw 38 } 39 40 // Layout updates the switch and displays it. 41 func (s SwitchStyle) Layout(gtx layout.Context) layout.Dimensions { 42 s.Switch.Update(gtx) 43 trackWidth := gtx.Dp(36) 44 trackHeight := gtx.Dp(16) 45 thumbSize := gtx.Dp(20) 46 trackOff := (thumbSize - trackHeight) / 2 47 48 // Draw track. 49 trackCorner := trackHeight / 2 50 trackRect := image.Rectangle{Max: image.Point{ 51 X: trackWidth, 52 Y: trackHeight, 53 }} 54 col := s.Color.Disabled 55 if s.Switch.Value { 56 col = s.Color.Enabled 57 } 58 if !gtx.Enabled() { 59 col = f32color.Disabled(col) 60 } 61 trackColor := s.Color.Track 62 t := op.Offset(image.Point{Y: trackOff}).Push(gtx.Ops) 63 cl := clip.UniformRRect(trackRect, trackCorner).Push(gtx.Ops) 64 paint.ColorOp{Color: trackColor}.Add(gtx.Ops) 65 paint.PaintOp{}.Add(gtx.Ops) 66 cl.Pop() 67 t.Pop() 68 69 // Draw thumb ink. 70 inkSize := gtx.Dp(44) 71 rr := inkSize / 2 72 inkOff := image.Point{ 73 X: trackWidth/2 - rr, 74 Y: -rr + trackHeight/2 + trackOff, 75 } 76 t = op.Offset(inkOff).Push(gtx.Ops) 77 gtx.Constraints.Min = image.Pt(inkSize, inkSize) 78 cl = clip.UniformRRect(image.Rectangle{Max: gtx.Constraints.Min}, rr).Push(gtx.Ops) 79 for _, p := range s.Switch.History() { 80 drawInk(gtx, p) 81 } 82 cl.Pop() 83 t.Pop() 84 85 // Compute thumb offset. 86 if s.Switch.Value { 87 xoff := trackWidth - thumbSize 88 defer op.Offset(image.Point{X: xoff}).Push(gtx.Ops).Pop() 89 } 90 91 thumbRadius := thumbSize / 2 92 93 circle := func(x, y, r int) clip.Op { 94 b := image.Rectangle{ 95 Min: image.Pt(x-r, y-r), 96 Max: image.Pt(x+r, y+r), 97 } 98 return clip.Ellipse(b).Op(gtx.Ops) 99 } 100 // Draw hover. 101 if s.Switch.Hovered() || gtx.Focused(s.Switch) { 102 r := thumbRadius * 10 / 17 103 background := f32color.MulAlpha(s.Color.Enabled, 70) 104 paint.FillShape(gtx.Ops, background, circle(thumbRadius, thumbRadius, r)) 105 } 106 107 // Draw thumb shadow, a translucent disc slightly larger than the 108 // thumb itself. 109 // Center shadow horizontally and slightly adjust its Y. 110 paint.FillShape(gtx.Ops, argb(0x55000000), circle(thumbRadius, thumbRadius+gtx.Dp(.25), thumbRadius+1)) 111 112 // Draw thumb. 113 paint.FillShape(gtx.Ops, col, circle(thumbRadius, thumbRadius, thumbRadius)) 114 115 // Set up click area. 116 clickSize := gtx.Dp(40) 117 clickOff := image.Point{ 118 X: (thumbSize - clickSize) / 2, 119 Y: (trackHeight-clickSize)/2 + trackOff, 120 } 121 defer op.Offset(clickOff).Push(gtx.Ops).Pop() 122 sz := image.Pt(clickSize, clickSize) 123 defer clip.Ellipse(image.Rectangle{Max: sz}).Push(gtx.Ops).Pop() 124 s.Switch.Layout(gtx, func(gtx layout.Context) layout.Dimensions { 125 if d := s.Description; d != "" { 126 semantic.DescriptionOp(d).Add(gtx.Ops) 127 } 128 semantic.Switch.Add(gtx.Ops) 129 return layout.Dimensions{Size: sz} 130 }) 131 132 dims := image.Point{X: trackWidth, Y: thumbSize} 133 return layout.Dimensions{Size: dims} 134 }