slider.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. package widget
  2. import (
  3. "fmt"
  4. "math"
  5. "fyne.io/fyne/v2"
  6. "fyne.io/fyne/v2/canvas"
  7. "fyne.io/fyne/v2/data/binding"
  8. "fyne.io/fyne/v2/internal/widget"
  9. "fyne.io/fyne/v2/theme"
  10. )
  11. // Orientation controls the horizontal/vertical layout of a widget
  12. type Orientation int
  13. // Orientation constants to control widget layout
  14. const (
  15. Horizontal Orientation = 0
  16. Vertical Orientation = 1
  17. )
  18. var _ fyne.Draggable = (*Slider)(nil)
  19. // Slider is a widget that can slide between two fixed values.
  20. type Slider struct {
  21. BaseWidget
  22. Value float64
  23. Min float64
  24. Max float64
  25. Step float64
  26. Orientation Orientation
  27. OnChanged func(float64)
  28. binder basicBinder
  29. }
  30. // NewSlider returns a basic slider.
  31. func NewSlider(min, max float64) *Slider {
  32. slider := &Slider{
  33. Value: 0,
  34. Min: min,
  35. Max: max,
  36. Step: 1,
  37. Orientation: Horizontal,
  38. }
  39. slider.ExtendBaseWidget(slider)
  40. return slider
  41. }
  42. // NewSliderWithData returns a slider connected with the specified data source.
  43. //
  44. // Since: 2.0
  45. func NewSliderWithData(min, max float64, data binding.Float) *Slider {
  46. slider := NewSlider(min, max)
  47. slider.Bind(data)
  48. return slider
  49. }
  50. // Bind connects the specified data source to this Slider.
  51. // The current value will be displayed and any changes in the data will cause the widget to update.
  52. // User interactions with this Slider will set the value into the data source.
  53. //
  54. // Since: 2.0
  55. func (s *Slider) Bind(data binding.Float) {
  56. s.binder.SetCallback(s.updateFromData)
  57. s.binder.Bind(data)
  58. s.OnChanged = func(_ float64) {
  59. s.binder.CallWithData(s.writeData)
  60. }
  61. }
  62. // DragEnd function.
  63. func (s *Slider) DragEnd() {
  64. }
  65. // Dragged function.
  66. func (s *Slider) Dragged(e *fyne.DragEvent) {
  67. ratio := s.getRatio(&(e.PointEvent))
  68. lastValue := s.Value
  69. s.updateValue(ratio)
  70. if s.almostEqual(lastValue, s.Value) {
  71. return
  72. }
  73. s.Refresh()
  74. if s.OnChanged != nil {
  75. s.OnChanged(s.Value)
  76. }
  77. }
  78. func (s *Slider) buttonDiameter() float32 {
  79. return theme.IconInlineSize() - 3.5 // match radio icons
  80. }
  81. func (s *Slider) endOffset() float32 {
  82. return s.buttonDiameter()/2 + theme.InnerPadding() - 1.5 // align with radio icons
  83. }
  84. func (s *Slider) getRatio(e *fyne.PointEvent) float64 {
  85. pad := s.endOffset()
  86. x := e.Position.X
  87. y := e.Position.Y
  88. switch s.Orientation {
  89. case Vertical:
  90. if y > s.size.Height-pad {
  91. return 0.0
  92. } else if y < pad {
  93. return 1.0
  94. } else {
  95. return 1 - float64(y-pad)/float64(s.size.Height-pad*2)
  96. }
  97. case Horizontal:
  98. if x > s.size.Width-pad {
  99. return 1.0
  100. } else if x < pad {
  101. return 0.0
  102. } else {
  103. return float64(x-pad) / float64(s.size.Width-pad*2)
  104. }
  105. }
  106. return 0.0
  107. }
  108. func (s *Slider) clampValueToRange() {
  109. if s.Value >= s.Max {
  110. s.Value = s.Max
  111. return
  112. } else if s.Value <= s.Min {
  113. s.Value = s.Min
  114. return
  115. }
  116. if s.Step == 0 { // extended Slider may not have this set - assume value is not adjusted
  117. return
  118. }
  119. rem := math.Mod(s.Value, s.Step)
  120. if rem == 0 {
  121. return
  122. }
  123. min := s.Value - rem
  124. if rem > s.Step/2 {
  125. min += s.Step
  126. }
  127. s.Value = min
  128. }
  129. func (s *Slider) updateValue(ratio float64) {
  130. s.Value = s.Min + ratio*(s.Max-s.Min)
  131. s.clampValueToRange()
  132. }
  133. // SetValue updates the value of the slider and clamps the value to be within the range.
  134. func (s *Slider) SetValue(value float64) {
  135. if s.Value == value {
  136. return
  137. }
  138. lastValue := s.Value
  139. s.Value = value
  140. s.clampValueToRange()
  141. if s.almostEqual(lastValue, s.Value) {
  142. return
  143. }
  144. if s.OnChanged != nil {
  145. s.OnChanged(s.Value)
  146. }
  147. s.Refresh()
  148. }
  149. // MinSize returns the size that this widget should not shrink below
  150. func (s *Slider) MinSize() fyne.Size {
  151. s.ExtendBaseWidget(s)
  152. return s.BaseWidget.MinSize()
  153. }
  154. // CreateRenderer links this widget to its renderer.
  155. func (s *Slider) CreateRenderer() fyne.WidgetRenderer {
  156. s.ExtendBaseWidget(s)
  157. track := canvas.NewRectangle(theme.InputBackgroundColor())
  158. active := canvas.NewRectangle(theme.ForegroundColor())
  159. thumb := &canvas.Circle{FillColor: theme.ForegroundColor()}
  160. objects := []fyne.CanvasObject{track, active, thumb}
  161. slide := &sliderRenderer{widget.NewBaseRenderer(objects), track, active, thumb, s}
  162. slide.Refresh() // prepare for first draw
  163. return slide
  164. }
  165. func (s *Slider) almostEqual(a, b float64) bool {
  166. delta := math.Abs(a - b)
  167. return delta <= s.Step/2
  168. }
  169. func (s *Slider) updateFromData(data binding.DataItem) {
  170. if data == nil {
  171. return
  172. }
  173. floatSource, ok := data.(binding.Float)
  174. if !ok {
  175. return
  176. }
  177. val, err := floatSource.Get()
  178. if err != nil {
  179. fyne.LogError("Error getting current data value", err)
  180. return
  181. }
  182. s.SetValue(val) // if val != s.Value, this will call updateFromData again, but only once
  183. }
  184. func (s *Slider) writeData(data binding.DataItem) {
  185. if data == nil {
  186. return
  187. }
  188. floatTarget, ok := data.(binding.Float)
  189. if !ok {
  190. return
  191. }
  192. currentValue, err := floatTarget.Get()
  193. if err != nil {
  194. return
  195. }
  196. if s.Value != currentValue {
  197. err := floatTarget.Set(s.Value)
  198. if err != nil {
  199. fyne.LogError(fmt.Sprintf("Failed to set binding value to %f", s.Value), err)
  200. }
  201. }
  202. }
  203. // Unbind disconnects any configured data source from this Slider.
  204. // The current value will remain at the last value of the data source.
  205. //
  206. // Since: 2.0
  207. func (s *Slider) Unbind() {
  208. s.OnChanged = nil
  209. s.binder.Unbind()
  210. }
  211. const (
  212. minLongSide = float32(34) // added to button diameter
  213. )
  214. type sliderRenderer struct {
  215. widget.BaseRenderer
  216. track *canvas.Rectangle
  217. active *canvas.Rectangle
  218. thumb *canvas.Circle
  219. slider *Slider
  220. }
  221. // Refresh updates the widget state for drawing.
  222. func (s *sliderRenderer) Refresh() {
  223. s.track.FillColor = theme.InputBackgroundColor()
  224. s.thumb.FillColor = theme.ForegroundColor()
  225. s.active.FillColor = theme.ForegroundColor()
  226. s.slider.clampValueToRange()
  227. s.Layout(s.slider.Size())
  228. canvas.Refresh(s.slider.super())
  229. }
  230. // Layout the components of the widget.
  231. func (s *sliderRenderer) Layout(size fyne.Size) {
  232. trackWidth := theme.InputBorderSize() * 2
  233. diameter := s.slider.buttonDiameter()
  234. endPad := s.slider.endOffset()
  235. var trackPos, activePos, thumbPos fyne.Position
  236. var trackSize, activeSize fyne.Size
  237. // some calculations are relative to trackSize, so we must update that first
  238. switch s.slider.Orientation {
  239. case Vertical:
  240. trackPos = fyne.NewPos(size.Width/2-theme.InputBorderSize(), endPad)
  241. trackSize = fyne.NewSize(trackWidth, size.Height-endPad*2)
  242. case Horizontal:
  243. trackPos = fyne.NewPos(endPad, size.Height/2-theme.InputBorderSize())
  244. trackSize = fyne.NewSize(size.Width-endPad*2, trackWidth)
  245. }
  246. s.track.Move(trackPos)
  247. s.track.Resize(trackSize)
  248. activeOffset := s.getOffset() // TODO based on old size...0
  249. switch s.slider.Orientation {
  250. case Vertical:
  251. activePos = fyne.NewPos(trackPos.X, activeOffset)
  252. activeSize = fyne.NewSize(trackWidth, trackSize.Height-activeOffset+endPad)
  253. thumbPos = fyne.NewPos(
  254. trackPos.X-(diameter-trackSize.Width)/2, activeOffset-(diameter/2))
  255. case Horizontal:
  256. activePos = trackPos
  257. activeSize = fyne.NewSize(activeOffset-endPad, trackWidth)
  258. thumbPos = fyne.NewPos(
  259. activeOffset-(diameter/2), trackPos.Y-(diameter-trackSize.Height)/2)
  260. }
  261. s.active.Move(activePos)
  262. s.active.Resize(activeSize)
  263. s.thumb.Move(thumbPos)
  264. s.thumb.Resize(fyne.NewSize(diameter, diameter))
  265. }
  266. // MinSize calculates the minimum size of a widget.
  267. func (s *sliderRenderer) MinSize() fyne.Size {
  268. dia := s.slider.buttonDiameter()
  269. s1, s2 := minLongSide+dia, dia
  270. switch s.slider.Orientation {
  271. case Vertical:
  272. return fyne.NewSize(s2, s1)
  273. case Horizontal:
  274. return fyne.NewSize(s1, s2)
  275. }
  276. return fyne.Size{Width: 0, Height: 0}
  277. }
  278. func (s *sliderRenderer) getOffset() float32 {
  279. endPad := s.slider.endOffset()
  280. w := s.slider
  281. size := s.track.Size()
  282. if w.Value == w.Min || w.Min == w.Max {
  283. switch w.Orientation {
  284. case Vertical:
  285. return size.Height + endPad
  286. case Horizontal:
  287. return endPad
  288. }
  289. }
  290. ratio := float32((w.Value - w.Min) / (w.Max - w.Min))
  291. switch w.Orientation {
  292. case Vertical:
  293. y := size.Height - ratio*size.Height + endPad
  294. return y
  295. case Horizontal:
  296. x := ratio*size.Width + endPad
  297. return x
  298. }
  299. return endPad
  300. }