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  }