slider.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. package widget
  2. import (
  3. "fmt"
  4. "image/color"
  5. "math"
  6. "fyne.io/fyne/v2"
  7. "fyne.io/fyne/v2/canvas"
  8. "fyne.io/fyne/v2/data/binding"
  9. "fyne.io/fyne/v2/driver/desktop"
  10. "fyne.io/fyne/v2/internal/widget"
  11. "fyne.io/fyne/v2/theme"
  12. )
  13. // Orientation controls the horizontal/vertical layout of a widget
  14. type Orientation int
  15. // Orientation constants to control widget layout
  16. const (
  17. Horizontal Orientation = 0
  18. Vertical Orientation = 1
  19. )
  20. var _ fyne.Draggable = (*Slider)(nil)
  21. var _ fyne.Focusable = (*Slider)(nil)
  22. var _ desktop.Hoverable = (*Slider)(nil)
  23. var _ fyne.Tappable = (*Slider)(nil)
  24. // Slider is a widget that can slide between two fixed values.
  25. type Slider struct {
  26. BaseWidget
  27. Value float64
  28. Min float64
  29. Max float64
  30. Step float64
  31. Orientation Orientation
  32. OnChanged func(float64)
  33. // Since: 2.4
  34. OnChangeEnded func(float64)
  35. binder basicBinder
  36. hovered bool
  37. focused bool
  38. pendingChange bool // true if value changed since last OnChangeEnded
  39. }
  40. // NewSlider returns a basic slider.
  41. func NewSlider(min, max float64) *Slider {
  42. slider := &Slider{
  43. Value: 0,
  44. Min: min,
  45. Max: max,
  46. Step: 1,
  47. Orientation: Horizontal,
  48. }
  49. slider.ExtendBaseWidget(slider)
  50. return slider
  51. }
  52. // NewSliderWithData returns a slider connected with the specified data source.
  53. //
  54. // Since: 2.0
  55. func NewSliderWithData(min, max float64, data binding.Float) *Slider {
  56. slider := NewSlider(min, max)
  57. slider.Bind(data)
  58. return slider
  59. }
  60. // Bind connects the specified data source to this Slider.
  61. // The current value will be displayed and any changes in the data will cause the widget to update.
  62. // User interactions with this Slider will set the value into the data source.
  63. //
  64. // Since: 2.0
  65. func (s *Slider) Bind(data binding.Float) {
  66. s.binder.SetCallback(s.updateFromData)
  67. s.binder.Bind(data)
  68. s.OnChanged = func(_ float64) {
  69. s.binder.CallWithData(s.writeData)
  70. }
  71. }
  72. // DragEnd is called when the drag ends.
  73. func (s *Slider) DragEnd() {
  74. s.fireChangeEnded()
  75. }
  76. // DragEnd is called when a drag event occurs.
  77. func (s *Slider) Dragged(e *fyne.DragEvent) {
  78. ratio := s.getRatio(&e.PointEvent)
  79. lastValue := s.Value
  80. s.updateValue(ratio)
  81. s.positionChanged(lastValue, s.Value)
  82. }
  83. // Tapped is called when a pointer tapped event is captured.
  84. //
  85. // Since: 2.4
  86. func (s *Slider) Tapped(e *fyne.PointEvent) {
  87. driver := fyne.CurrentApp().Driver()
  88. if !s.focused && !driver.Device().IsMobile() {
  89. impl := s.super()
  90. if c := driver.CanvasForObject(impl); c != nil {
  91. c.Focus(impl.(fyne.Focusable))
  92. }
  93. }
  94. ratio := s.getRatio(e)
  95. lastValue := s.Value
  96. s.updateValue(ratio)
  97. s.positionChanged(lastValue, s.Value)
  98. s.fireChangeEnded()
  99. }
  100. func (s *Slider) positionChanged(lastValue, currentValue float64) {
  101. if s.almostEqual(lastValue, currentValue) {
  102. return
  103. }
  104. s.Refresh()
  105. s.pendingChange = true
  106. if s.OnChanged != nil {
  107. s.OnChanged(s.Value)
  108. }
  109. }
  110. func (s *Slider) fireChangeEnded() {
  111. if !s.pendingChange {
  112. return
  113. }
  114. s.pendingChange = false
  115. if s.OnChangeEnded != nil {
  116. s.OnChangeEnded(s.Value)
  117. }
  118. }
  119. // FocusGained is called when this item gained the focus.
  120. //
  121. // Since: 2.4
  122. func (s *Slider) FocusGained() {
  123. s.focused = true
  124. s.Refresh()
  125. }
  126. // FocusLost is called when this item lost the focus.
  127. //
  128. // Since: 2.4
  129. func (s *Slider) FocusLost() {
  130. s.focused = false
  131. s.Refresh()
  132. }
  133. // MouseIn is called when a desktop pointer enters the widget.
  134. //
  135. // Since: 2.4
  136. func (s *Slider) MouseIn(_ *desktop.MouseEvent) {
  137. s.hovered = true
  138. s.Refresh()
  139. }
  140. // MouseMoved is called when a desktop pointer hovers over the widget.
  141. //
  142. // Since: 2.4
  143. func (s *Slider) MouseMoved(_ *desktop.MouseEvent) {
  144. }
  145. // MouseOut is called when a desktop pointer exits the widget
  146. //
  147. // Since: 2.4
  148. func (s *Slider) MouseOut() {
  149. s.hovered = false
  150. s.Refresh()
  151. }
  152. // TypedKey is called when this item receives a key event.
  153. //
  154. // Since: 2.4
  155. func (s *Slider) TypedKey(key *fyne.KeyEvent) {
  156. if s.Orientation == Vertical {
  157. switch key.Name {
  158. case fyne.KeyUp:
  159. s.SetValue(s.Value + s.Step)
  160. case fyne.KeyDown:
  161. s.SetValue(s.Value - s.Step)
  162. }
  163. } else {
  164. switch key.Name {
  165. case fyne.KeyLeft:
  166. s.SetValue(s.Value - s.Step)
  167. case fyne.KeyRight:
  168. s.SetValue(s.Value + s.Step)
  169. }
  170. }
  171. }
  172. // TypedRune is called when this item receives a char event.
  173. //
  174. // Since: 2.4
  175. func (s *Slider) TypedRune(_ rune) {
  176. }
  177. func (s *Slider) buttonDiameter() float32 {
  178. return theme.IconInlineSize() - 4 // match radio icons
  179. }
  180. func (s *Slider) endOffset() float32 {
  181. return s.buttonDiameter()/2 + theme.InnerPadding() - 1.5 // align with radio icons
  182. }
  183. func (s *Slider) getRatio(e *fyne.PointEvent) float64 {
  184. pad := s.endOffset()
  185. x := e.Position.X
  186. y := e.Position.Y
  187. switch s.Orientation {
  188. case Vertical:
  189. if y > s.size.Height-pad {
  190. return 0.0
  191. } else if y < pad {
  192. return 1.0
  193. } else {
  194. return 1 - float64(y-pad)/float64(s.size.Height-pad*2)
  195. }
  196. case Horizontal:
  197. if x > s.size.Width-pad {
  198. return 1.0
  199. } else if x < pad {
  200. return 0.0
  201. } else {
  202. return float64(x-pad) / float64(s.size.Width-pad*2)
  203. }
  204. }
  205. return 0.0
  206. }
  207. func (s *Slider) clampValueToRange() {
  208. if s.Value >= s.Max {
  209. s.Value = s.Max
  210. return
  211. } else if s.Value <= s.Min {
  212. s.Value = s.Min
  213. return
  214. }
  215. if s.Step == 0 { // extended Slider may not have this set - assume value is not adjusted
  216. return
  217. }
  218. rem := math.Mod(s.Value, s.Step)
  219. if rem == 0 {
  220. return
  221. }
  222. min := s.Value - rem
  223. if rem > s.Step/2 {
  224. min += s.Step
  225. }
  226. s.Value = min
  227. }
  228. func (s *Slider) updateValue(ratio float64) {
  229. s.Value = s.Min + ratio*(s.Max-s.Min)
  230. s.clampValueToRange()
  231. }
  232. // SetValue updates the value of the slider and clamps the value to be within the range.
  233. func (s *Slider) SetValue(value float64) {
  234. if s.Value == value {
  235. return
  236. }
  237. lastValue := s.Value
  238. s.Value = value
  239. s.clampValueToRange()
  240. s.positionChanged(lastValue, s.Value)
  241. s.fireChangeEnded()
  242. }
  243. // MinSize returns the size that this widget should not shrink below
  244. func (s *Slider) MinSize() fyne.Size {
  245. s.ExtendBaseWidget(s)
  246. return s.BaseWidget.MinSize()
  247. }
  248. // CreateRenderer links this widget to its renderer.
  249. func (s *Slider) CreateRenderer() fyne.WidgetRenderer {
  250. s.ExtendBaseWidget(s)
  251. track := canvas.NewRectangle(theme.InputBackgroundColor())
  252. active := canvas.NewRectangle(theme.ForegroundColor())
  253. thumb := &canvas.Circle{FillColor: theme.ForegroundColor()}
  254. focusIndicator := &canvas.Circle{FillColor: color.Transparent}
  255. objects := []fyne.CanvasObject{track, active, thumb, focusIndicator}
  256. slide := &sliderRenderer{widget.NewBaseRenderer(objects), track, active, thumb, focusIndicator, s}
  257. slide.Refresh() // prepare for first draw
  258. return slide
  259. }
  260. func (s *Slider) almostEqual(a, b float64) bool {
  261. delta := math.Abs(a - b)
  262. return delta <= s.Step/2
  263. }
  264. func (s *Slider) updateFromData(data binding.DataItem) {
  265. if data == nil {
  266. return
  267. }
  268. floatSource, ok := data.(binding.Float)
  269. if !ok {
  270. return
  271. }
  272. val, err := floatSource.Get()
  273. if err != nil {
  274. fyne.LogError("Error getting current data value", err)
  275. return
  276. }
  277. s.SetValue(val) // if val != s.Value, this will call updateFromData again, but only once
  278. }
  279. func (s *Slider) writeData(data binding.DataItem) {
  280. if data == nil {
  281. return
  282. }
  283. floatTarget, ok := data.(binding.Float)
  284. if !ok {
  285. return
  286. }
  287. currentValue, err := floatTarget.Get()
  288. if err != nil {
  289. return
  290. }
  291. if s.Value != currentValue {
  292. err := floatTarget.Set(s.Value)
  293. if err != nil {
  294. fyne.LogError(fmt.Sprintf("Failed to set binding value to %f", s.Value), err)
  295. }
  296. }
  297. }
  298. // Unbind disconnects any configured data source from this Slider.
  299. // The current value will remain at the last value of the data source.
  300. //
  301. // Since: 2.0
  302. func (s *Slider) Unbind() {
  303. s.OnChanged = nil
  304. s.binder.Unbind()
  305. }
  306. const minLongSide = float32(34) // added to button diameter
  307. type sliderRenderer struct {
  308. widget.BaseRenderer
  309. track *canvas.Rectangle
  310. active *canvas.Rectangle
  311. thumb *canvas.Circle
  312. focusIndicator *canvas.Circle
  313. slider *Slider
  314. }
  315. // Refresh updates the widget state for drawing.
  316. func (s *sliderRenderer) Refresh() {
  317. s.track.FillColor = theme.InputBackgroundColor()
  318. s.thumb.FillColor = theme.ForegroundColor()
  319. s.active.FillColor = theme.ForegroundColor()
  320. if s.slider.focused {
  321. s.focusIndicator.FillColor = theme.FocusColor()
  322. } else if s.slider.hovered {
  323. s.focusIndicator.FillColor = theme.HoverColor()
  324. } else {
  325. s.focusIndicator.FillColor = color.Transparent
  326. }
  327. s.focusIndicator.Refresh()
  328. s.slider.clampValueToRange()
  329. s.Layout(s.slider.Size())
  330. canvas.Refresh(s.slider.super())
  331. }
  332. // Layout the components of the widget.
  333. func (s *sliderRenderer) Layout(size fyne.Size) {
  334. trackWidth := theme.InputBorderSize() * 2
  335. diameter := s.slider.buttonDiameter()
  336. endPad := s.slider.endOffset()
  337. var trackPos, activePos, thumbPos fyne.Position
  338. var trackSize, activeSize fyne.Size
  339. // some calculations are relative to trackSize, so we must update that first
  340. switch s.slider.Orientation {
  341. case Vertical:
  342. trackPos = fyne.NewPos(size.Width/2-theme.InputBorderSize(), endPad)
  343. trackSize = fyne.NewSize(trackWidth, size.Height-endPad*2)
  344. case Horizontal:
  345. trackPos = fyne.NewPos(endPad, size.Height/2-theme.InputBorderSize())
  346. trackSize = fyne.NewSize(size.Width-endPad*2, trackWidth)
  347. }
  348. s.track.Move(trackPos)
  349. s.track.Resize(trackSize)
  350. activeOffset := s.getOffset() // TODO based on old size...0
  351. switch s.slider.Orientation {
  352. case Vertical:
  353. activePos = fyne.NewPos(trackPos.X, activeOffset)
  354. activeSize = fyne.NewSize(trackWidth, trackSize.Height-activeOffset+endPad)
  355. thumbPos = fyne.NewPos(
  356. trackPos.X-(diameter-trackSize.Width)/2, activeOffset-(diameter/2))
  357. case Horizontal:
  358. activePos = trackPos
  359. activeSize = fyne.NewSize(activeOffset-endPad, trackWidth)
  360. thumbPos = fyne.NewPos(
  361. activeOffset-(diameter/2), trackPos.Y-(diameter-trackSize.Height)/2)
  362. }
  363. s.active.Move(activePos)
  364. s.active.Resize(activeSize)
  365. s.thumb.Move(thumbPos)
  366. s.thumb.Resize(fyne.NewSize(diameter, diameter))
  367. focusIndicatorSize := fyne.NewSquareSize(theme.IconInlineSize() + theme.InnerPadding())
  368. delta := (focusIndicatorSize.Width - diameter) / 2
  369. s.focusIndicator.Resize(focusIndicatorSize)
  370. s.focusIndicator.Move(thumbPos.SubtractXY(delta, delta))
  371. }
  372. // MinSize calculates the minimum size of a widget.
  373. func (s *sliderRenderer) MinSize() fyne.Size {
  374. dia := s.slider.buttonDiameter()
  375. s1, s2 := minLongSide+dia, dia
  376. switch s.slider.Orientation {
  377. case Vertical:
  378. return fyne.NewSize(s2, s1)
  379. case Horizontal:
  380. return fyne.NewSize(s1, s2)
  381. }
  382. return fyne.Size{Width: 0, Height: 0}
  383. }
  384. func (s *sliderRenderer) getOffset() float32 {
  385. endPad := s.slider.endOffset()
  386. w := s.slider
  387. size := s.track.Size()
  388. if w.Value == w.Min || w.Min == w.Max {
  389. switch w.Orientation {
  390. case Vertical:
  391. return size.Height + endPad
  392. case Horizontal:
  393. return endPad
  394. }
  395. }
  396. ratio := float32((w.Value - w.Min) / (w.Max - w.Min))
  397. switch w.Orientation {
  398. case Vertical:
  399. y := size.Height - ratio*size.Height + endPad
  400. return y
  401. case Horizontal:
  402. x := ratio*size.Width + endPad
  403. return x
  404. }
  405. return endPad
  406. }