| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- package widget
- import (
- "fmt"
- "image/color"
- "math"
- "fyne.io/fyne/v2"
- "fyne.io/fyne/v2/canvas"
- "fyne.io/fyne/v2/data/binding"
- "fyne.io/fyne/v2/driver/desktop"
- "fyne.io/fyne/v2/internal/widget"
- "fyne.io/fyne/v2/theme"
- )
- // Orientation controls the horizontal/vertical layout of a widget
- type Orientation int
- // Orientation constants to control widget layout
- const (
- Horizontal Orientation = 0
- Vertical Orientation = 1
- )
- var _ fyne.Draggable = (*Slider)(nil)
- var _ fyne.Focusable = (*Slider)(nil)
- var _ desktop.Hoverable = (*Slider)(nil)
- var _ fyne.Tappable = (*Slider)(nil)
- // Slider is a widget that can slide between two fixed values.
- type Slider struct {
- BaseWidget
- Value float64
- Min float64
- Max float64
- Step float64
- Orientation Orientation
- OnChanged func(float64)
- // Since: 2.4
- OnChangeEnded func(float64)
- binder basicBinder
- hovered bool
- focused bool
- pendingChange bool // true if value changed since last OnChangeEnded
- }
- // NewSlider returns a basic slider.
- func NewSlider(min, max float64) *Slider {
- slider := &Slider{
- Value: 0,
- Min: min,
- Max: max,
- Step: 1,
- Orientation: Horizontal,
- }
- slider.ExtendBaseWidget(slider)
- return slider
- }
- // NewSliderWithData returns a slider connected with the specified data source.
- //
- // Since: 2.0
- func NewSliderWithData(min, max float64, data binding.Float) *Slider {
- slider := NewSlider(min, max)
- slider.Bind(data)
- return slider
- }
- // Bind connects the specified data source to this Slider.
- // The current value will be displayed and any changes in the data will cause the widget to update.
- // User interactions with this Slider will set the value into the data source.
- //
- // Since: 2.0
- func (s *Slider) Bind(data binding.Float) {
- s.binder.SetCallback(s.updateFromData)
- s.binder.Bind(data)
- s.OnChanged = func(_ float64) {
- s.binder.CallWithData(s.writeData)
- }
- }
- // DragEnd is called when the drag ends.
- func (s *Slider) DragEnd() {
- s.fireChangeEnded()
- }
- // DragEnd is called when a drag event occurs.
- func (s *Slider) Dragged(e *fyne.DragEvent) {
- ratio := s.getRatio(&e.PointEvent)
- lastValue := s.Value
- s.updateValue(ratio)
- s.positionChanged(lastValue, s.Value)
- }
- // Tapped is called when a pointer tapped event is captured.
- //
- // Since: 2.4
- func (s *Slider) Tapped(e *fyne.PointEvent) {
- driver := fyne.CurrentApp().Driver()
- if !s.focused && !driver.Device().IsMobile() {
- impl := s.super()
- if c := driver.CanvasForObject(impl); c != nil {
- c.Focus(impl.(fyne.Focusable))
- }
- }
- ratio := s.getRatio(e)
- lastValue := s.Value
- s.updateValue(ratio)
- s.positionChanged(lastValue, s.Value)
- s.fireChangeEnded()
- }
- func (s *Slider) positionChanged(lastValue, currentValue float64) {
- if s.almostEqual(lastValue, currentValue) {
- return
- }
- s.Refresh()
- s.pendingChange = true
- if s.OnChanged != nil {
- s.OnChanged(s.Value)
- }
- }
- func (s *Slider) fireChangeEnded() {
- if !s.pendingChange {
- return
- }
- s.pendingChange = false
- if s.OnChangeEnded != nil {
- s.OnChangeEnded(s.Value)
- }
- }
- // FocusGained is called when this item gained the focus.
- //
- // Since: 2.4
- func (s *Slider) FocusGained() {
- s.focused = true
- s.Refresh()
- }
- // FocusLost is called when this item lost the focus.
- //
- // Since: 2.4
- func (s *Slider) FocusLost() {
- s.focused = false
- s.Refresh()
- }
- // MouseIn is called when a desktop pointer enters the widget.
- //
- // Since: 2.4
- func (s *Slider) MouseIn(_ *desktop.MouseEvent) {
- s.hovered = true
- s.Refresh()
- }
- // MouseMoved is called when a desktop pointer hovers over the widget.
- //
- // Since: 2.4
- func (s *Slider) MouseMoved(_ *desktop.MouseEvent) {
- }
- // MouseOut is called when a desktop pointer exits the widget
- //
- // Since: 2.4
- func (s *Slider) MouseOut() {
- s.hovered = false
- s.Refresh()
- }
- // TypedKey is called when this item receives a key event.
- //
- // Since: 2.4
- func (s *Slider) TypedKey(key *fyne.KeyEvent) {
- if s.Orientation == Vertical {
- switch key.Name {
- case fyne.KeyUp:
- s.SetValue(s.Value + s.Step)
- case fyne.KeyDown:
- s.SetValue(s.Value - s.Step)
- }
- } else {
- switch key.Name {
- case fyne.KeyLeft:
- s.SetValue(s.Value - s.Step)
- case fyne.KeyRight:
- s.SetValue(s.Value + s.Step)
- }
- }
- }
- // TypedRune is called when this item receives a char event.
- //
- // Since: 2.4
- func (s *Slider) TypedRune(_ rune) {
- }
- func (s *Slider) buttonDiameter() float32 {
- return theme.IconInlineSize() - 4 // match radio icons
- }
- func (s *Slider) endOffset() float32 {
- return s.buttonDiameter()/2 + theme.InnerPadding() - 1.5 // align with radio icons
- }
- func (s *Slider) getRatio(e *fyne.PointEvent) float64 {
- pad := s.endOffset()
- x := e.Position.X
- y := e.Position.Y
- switch s.Orientation {
- case Vertical:
- if y > s.size.Height-pad {
- return 0.0
- } else if y < pad {
- return 1.0
- } else {
- return 1 - float64(y-pad)/float64(s.size.Height-pad*2)
- }
- case Horizontal:
- if x > s.size.Width-pad {
- return 1.0
- } else if x < pad {
- return 0.0
- } else {
- return float64(x-pad) / float64(s.size.Width-pad*2)
- }
- }
- return 0.0
- }
- func (s *Slider) clampValueToRange() {
- if s.Value >= s.Max {
- s.Value = s.Max
- return
- } else if s.Value <= s.Min {
- s.Value = s.Min
- return
- }
- if s.Step == 0 { // extended Slider may not have this set - assume value is not adjusted
- return
- }
- rem := math.Mod(s.Value, s.Step)
- if rem == 0 {
- return
- }
- min := s.Value - rem
- if rem > s.Step/2 {
- min += s.Step
- }
- s.Value = min
- }
- func (s *Slider) updateValue(ratio float64) {
- s.Value = s.Min + ratio*(s.Max-s.Min)
- s.clampValueToRange()
- }
- // SetValue updates the value of the slider and clamps the value to be within the range.
- func (s *Slider) SetValue(value float64) {
- if s.Value == value {
- return
- }
- lastValue := s.Value
- s.Value = value
- s.clampValueToRange()
- s.positionChanged(lastValue, s.Value)
- s.fireChangeEnded()
- }
- // MinSize returns the size that this widget should not shrink below
- func (s *Slider) MinSize() fyne.Size {
- s.ExtendBaseWidget(s)
- return s.BaseWidget.MinSize()
- }
- // CreateRenderer links this widget to its renderer.
- func (s *Slider) CreateRenderer() fyne.WidgetRenderer {
- s.ExtendBaseWidget(s)
- track := canvas.NewRectangle(theme.InputBackgroundColor())
- active := canvas.NewRectangle(theme.ForegroundColor())
- thumb := &canvas.Circle{FillColor: theme.ForegroundColor()}
- focusIndicator := &canvas.Circle{FillColor: color.Transparent}
- objects := []fyne.CanvasObject{track, active, thumb, focusIndicator}
- slide := &sliderRenderer{widget.NewBaseRenderer(objects), track, active, thumb, focusIndicator, s}
- slide.Refresh() // prepare for first draw
- return slide
- }
- func (s *Slider) almostEqual(a, b float64) bool {
- delta := math.Abs(a - b)
- return delta <= s.Step/2
- }
- func (s *Slider) updateFromData(data binding.DataItem) {
- if data == nil {
- return
- }
- floatSource, ok := data.(binding.Float)
- if !ok {
- return
- }
- val, err := floatSource.Get()
- if err != nil {
- fyne.LogError("Error getting current data value", err)
- return
- }
- s.SetValue(val) // if val != s.Value, this will call updateFromData again, but only once
- }
- func (s *Slider) writeData(data binding.DataItem) {
- if data == nil {
- return
- }
- floatTarget, ok := data.(binding.Float)
- if !ok {
- return
- }
- currentValue, err := floatTarget.Get()
- if err != nil {
- return
- }
- if s.Value != currentValue {
- err := floatTarget.Set(s.Value)
- if err != nil {
- fyne.LogError(fmt.Sprintf("Failed to set binding value to %f", s.Value), err)
- }
- }
- }
- // Unbind disconnects any configured data source from this Slider.
- // The current value will remain at the last value of the data source.
- //
- // Since: 2.0
- func (s *Slider) Unbind() {
- s.OnChanged = nil
- s.binder.Unbind()
- }
- const minLongSide = float32(34) // added to button diameter
- type sliderRenderer struct {
- widget.BaseRenderer
- track *canvas.Rectangle
- active *canvas.Rectangle
- thumb *canvas.Circle
- focusIndicator *canvas.Circle
- slider *Slider
- }
- // Refresh updates the widget state for drawing.
- func (s *sliderRenderer) Refresh() {
- s.track.FillColor = theme.InputBackgroundColor()
- s.thumb.FillColor = theme.ForegroundColor()
- s.active.FillColor = theme.ForegroundColor()
- if s.slider.focused {
- s.focusIndicator.FillColor = theme.FocusColor()
- } else if s.slider.hovered {
- s.focusIndicator.FillColor = theme.HoverColor()
- } else {
- s.focusIndicator.FillColor = color.Transparent
- }
- s.focusIndicator.Refresh()
- s.slider.clampValueToRange()
- s.Layout(s.slider.Size())
- canvas.Refresh(s.slider.super())
- }
- // Layout the components of the widget.
- func (s *sliderRenderer) Layout(size fyne.Size) {
- trackWidth := theme.InputBorderSize() * 2
- diameter := s.slider.buttonDiameter()
- endPad := s.slider.endOffset()
- var trackPos, activePos, thumbPos fyne.Position
- var trackSize, activeSize fyne.Size
- // some calculations are relative to trackSize, so we must update that first
- switch s.slider.Orientation {
- case Vertical:
- trackPos = fyne.NewPos(size.Width/2-theme.InputBorderSize(), endPad)
- trackSize = fyne.NewSize(trackWidth, size.Height-endPad*2)
- case Horizontal:
- trackPos = fyne.NewPos(endPad, size.Height/2-theme.InputBorderSize())
- trackSize = fyne.NewSize(size.Width-endPad*2, trackWidth)
- }
- s.track.Move(trackPos)
- s.track.Resize(trackSize)
- activeOffset := s.getOffset() // TODO based on old size...0
- switch s.slider.Orientation {
- case Vertical:
- activePos = fyne.NewPos(trackPos.X, activeOffset)
- activeSize = fyne.NewSize(trackWidth, trackSize.Height-activeOffset+endPad)
- thumbPos = fyne.NewPos(
- trackPos.X-(diameter-trackSize.Width)/2, activeOffset-(diameter/2))
- case Horizontal:
- activePos = trackPos
- activeSize = fyne.NewSize(activeOffset-endPad, trackWidth)
- thumbPos = fyne.NewPos(
- activeOffset-(diameter/2), trackPos.Y-(diameter-trackSize.Height)/2)
- }
- s.active.Move(activePos)
- s.active.Resize(activeSize)
- s.thumb.Move(thumbPos)
- s.thumb.Resize(fyne.NewSize(diameter, diameter))
- focusIndicatorSize := fyne.NewSquareSize(theme.IconInlineSize() + theme.InnerPadding())
- delta := (focusIndicatorSize.Width - diameter) / 2
- s.focusIndicator.Resize(focusIndicatorSize)
- s.focusIndicator.Move(thumbPos.SubtractXY(delta, delta))
- }
- // MinSize calculates the minimum size of a widget.
- func (s *sliderRenderer) MinSize() fyne.Size {
- dia := s.slider.buttonDiameter()
- s1, s2 := minLongSide+dia, dia
- switch s.slider.Orientation {
- case Vertical:
- return fyne.NewSize(s2, s1)
- case Horizontal:
- return fyne.NewSize(s1, s2)
- }
- return fyne.Size{Width: 0, Height: 0}
- }
- func (s *sliderRenderer) getOffset() float32 {
- endPad := s.slider.endOffset()
- w := s.slider
- size := s.track.Size()
- if w.Value == w.Min || w.Min == w.Max {
- switch w.Orientation {
- case Vertical:
- return size.Height + endPad
- case Horizontal:
- return endPad
- }
- }
- ratio := float32((w.Value - w.Min) / (w.Max - w.Min))
- switch w.Orientation {
- case Vertical:
- y := size.Height - ratio*size.Height + endPad
- return y
- case Horizontal:
- x := ratio*size.Width + endPad
- return x
- }
- return endPad
- }
|