scroller.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. package widget
  2. import (
  3. "fyne.io/fyne/v2"
  4. "fyne.io/fyne/v2/canvas"
  5. "fyne.io/fyne/v2/driver/desktop"
  6. "fyne.io/fyne/v2/internal/cache"
  7. "fyne.io/fyne/v2/theme"
  8. )
  9. // ScrollDirection represents the directions in which a Scroll can scroll its child content.
  10. type ScrollDirection int
  11. // Constants for valid values of ScrollDirection.
  12. const (
  13. // ScrollBoth supports horizontal and vertical scrolling.
  14. ScrollBoth ScrollDirection = iota
  15. // ScrollHorizontalOnly specifies the scrolling should only happen left to right.
  16. ScrollHorizontalOnly
  17. // ScrollVerticalOnly specifies the scrolling should only happen top to bottom.
  18. ScrollVerticalOnly
  19. // ScrollNone turns off scrolling for this container.
  20. //
  21. // Since: 2.0
  22. ScrollNone
  23. )
  24. type scrollBarOrientation int
  25. // We default to vertical as 0 due to that being the original orientation offered
  26. const (
  27. scrollBarOrientationVertical scrollBarOrientation = 0
  28. scrollBarOrientationHorizontal scrollBarOrientation = 1
  29. scrollContainerMinSize = float32(32) // TODO consider the smallest useful scroll view?
  30. )
  31. type scrollBarRenderer struct {
  32. BaseRenderer
  33. scrollBar *scrollBar
  34. background *canvas.Rectangle
  35. minSize fyne.Size
  36. }
  37. func (r *scrollBarRenderer) Layout(size fyne.Size) {
  38. r.background.Resize(size)
  39. }
  40. func (r *scrollBarRenderer) MinSize() fyne.Size {
  41. return r.minSize
  42. }
  43. func (r *scrollBarRenderer) Refresh() {
  44. r.background.FillColor = theme.ScrollBarColor()
  45. r.background.Refresh()
  46. }
  47. var _ desktop.Hoverable = (*scrollBar)(nil)
  48. var _ fyne.Draggable = (*scrollBar)(nil)
  49. type scrollBar struct {
  50. Base
  51. area *scrollBarArea
  52. draggedDistance float32
  53. dragStart float32
  54. isDragged bool
  55. orientation scrollBarOrientation
  56. }
  57. func (b *scrollBar) CreateRenderer() fyne.WidgetRenderer {
  58. background := canvas.NewRectangle(theme.ScrollBarColor())
  59. r := &scrollBarRenderer{
  60. scrollBar: b,
  61. background: background,
  62. }
  63. r.SetObjects([]fyne.CanvasObject{background})
  64. return r
  65. }
  66. func (b *scrollBar) Cursor() desktop.Cursor {
  67. return desktop.DefaultCursor
  68. }
  69. func (b *scrollBar) DragEnd() {
  70. b.isDragged = false
  71. }
  72. func (b *scrollBar) Dragged(e *fyne.DragEvent) {
  73. if !b.isDragged {
  74. b.isDragged = true
  75. switch b.orientation {
  76. case scrollBarOrientationHorizontal:
  77. b.dragStart = b.Position().X
  78. case scrollBarOrientationVertical:
  79. b.dragStart = b.Position().Y
  80. }
  81. b.draggedDistance = 0
  82. }
  83. switch b.orientation {
  84. case scrollBarOrientationHorizontal:
  85. b.draggedDistance += e.Dragged.DX
  86. case scrollBarOrientationVertical:
  87. b.draggedDistance += e.Dragged.DY
  88. }
  89. b.area.moveBar(b.draggedDistance+b.dragStart, b.Size())
  90. }
  91. func (b *scrollBar) MouseIn(e *desktop.MouseEvent) {
  92. b.area.MouseIn(e)
  93. }
  94. func (b *scrollBar) MouseMoved(*desktop.MouseEvent) {
  95. }
  96. func (b *scrollBar) MouseOut() {
  97. b.area.MouseOut()
  98. }
  99. func newScrollBar(area *scrollBarArea) *scrollBar {
  100. b := &scrollBar{area: area, orientation: area.orientation}
  101. b.ExtendBaseWidget(b)
  102. return b
  103. }
  104. type scrollBarAreaRenderer struct {
  105. BaseRenderer
  106. area *scrollBarArea
  107. bar *scrollBar
  108. }
  109. func (r *scrollBarAreaRenderer) Layout(_ fyne.Size) {
  110. var barHeight, barWidth, barX, barY float32
  111. switch r.area.orientation {
  112. case scrollBarOrientationHorizontal:
  113. barWidth, barHeight, barX, barY = r.barSizeAndOffset(r.area.scroll.Offset.X, r.area.scroll.Content.Size().Width, r.area.scroll.Size().Width)
  114. default:
  115. barHeight, barWidth, barY, barX = r.barSizeAndOffset(r.area.scroll.Offset.Y, r.area.scroll.Content.Size().Height, r.area.scroll.Size().Height)
  116. }
  117. r.bar.Move(fyne.NewPos(barX, barY))
  118. r.bar.Resize(fyne.NewSize(barWidth, barHeight))
  119. }
  120. func (r *scrollBarAreaRenderer) MinSize() fyne.Size {
  121. min := theme.ScrollBarSize()
  122. if !r.area.isLarge {
  123. min = theme.ScrollBarSmallSize() * 2
  124. }
  125. switch r.area.orientation {
  126. case scrollBarOrientationHorizontal:
  127. return fyne.NewSize(theme.ScrollBarSize(), min)
  128. default:
  129. return fyne.NewSize(min, theme.ScrollBarSize())
  130. }
  131. }
  132. func (r *scrollBarAreaRenderer) Refresh() {
  133. r.Layout(r.area.Size())
  134. canvas.Refresh(r.bar)
  135. }
  136. func (r *scrollBarAreaRenderer) barSizeAndOffset(contentOffset, contentLength, scrollLength float32) (length, width, lengthOffset, widthOffset float32) {
  137. if scrollLength < contentLength {
  138. portion := scrollLength / contentLength
  139. length = float32(int(scrollLength)) * portion
  140. if length < theme.ScrollBarSize() {
  141. length = theme.ScrollBarSize()
  142. }
  143. } else {
  144. length = scrollLength
  145. }
  146. if contentOffset != 0 {
  147. lengthOffset = (scrollLength - length) * (contentOffset / (contentLength - scrollLength))
  148. }
  149. if r.area.isLarge {
  150. width = theme.ScrollBarSize()
  151. } else {
  152. widthOffset = theme.ScrollBarSmallSize()
  153. width = theme.ScrollBarSmallSize()
  154. }
  155. return
  156. }
  157. var _ desktop.Hoverable = (*scrollBarArea)(nil)
  158. type scrollBarArea struct {
  159. Base
  160. isLarge bool
  161. scroll *Scroll
  162. orientation scrollBarOrientation
  163. }
  164. func (a *scrollBarArea) CreateRenderer() fyne.WidgetRenderer {
  165. bar := newScrollBar(a)
  166. return &scrollBarAreaRenderer{BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{bar}), area: a, bar: bar}
  167. }
  168. func (a *scrollBarArea) MouseIn(*desktop.MouseEvent) {
  169. a.isLarge = true
  170. a.scroll.Refresh()
  171. }
  172. func (a *scrollBarArea) MouseMoved(*desktop.MouseEvent) {
  173. }
  174. func (a *scrollBarArea) MouseOut() {
  175. a.isLarge = false
  176. a.scroll.Refresh()
  177. }
  178. func (a *scrollBarArea) moveBar(offset float32, barSize fyne.Size) {
  179. oldX := a.scroll.Offset.X
  180. oldY := a.scroll.Offset.Y
  181. switch a.orientation {
  182. case scrollBarOrientationHorizontal:
  183. a.scroll.Offset.X = a.computeScrollOffset(barSize.Width, offset, a.scroll.Size().Width, a.scroll.Content.Size().Width)
  184. default:
  185. a.scroll.Offset.Y = a.computeScrollOffset(barSize.Height, offset, a.scroll.Size().Height, a.scroll.Content.Size().Height)
  186. }
  187. if f := a.scroll.OnScrolled; f != nil && (a.scroll.Offset.X != oldX || a.scroll.Offset.Y != oldY) {
  188. f(a.scroll.Offset)
  189. }
  190. a.scroll.refreshWithoutOffsetUpdate()
  191. }
  192. func (a *scrollBarArea) computeScrollOffset(length, offset, scrollLength, contentLength float32) float32 {
  193. maxOffset := scrollLength - length
  194. if offset < 0 {
  195. offset = 0
  196. } else if offset > maxOffset {
  197. offset = maxOffset
  198. }
  199. ratio := offset / maxOffset
  200. scrollOffset := ratio * (contentLength - scrollLength)
  201. return scrollOffset
  202. }
  203. func newScrollBarArea(scroll *Scroll, orientation scrollBarOrientation) *scrollBarArea {
  204. a := &scrollBarArea{scroll: scroll, orientation: orientation}
  205. a.ExtendBaseWidget(a)
  206. return a
  207. }
  208. type scrollContainerRenderer struct {
  209. BaseRenderer
  210. scroll *Scroll
  211. vertArea *scrollBarArea
  212. horizArea *scrollBarArea
  213. leftShadow, rightShadow *Shadow
  214. topShadow, bottomShadow *Shadow
  215. oldMinSize fyne.Size
  216. }
  217. func (r *scrollContainerRenderer) layoutBars(size fyne.Size) {
  218. if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth {
  219. r.vertArea.Resize(fyne.NewSize(r.vertArea.MinSize().Width, size.Height))
  220. r.vertArea.Move(fyne.NewPos(r.scroll.Size().Width-r.vertArea.Size().Width, 0))
  221. r.topShadow.Resize(fyne.NewSize(size.Width, 0))
  222. r.bottomShadow.Resize(fyne.NewSize(size.Width, 0))
  223. r.bottomShadow.Move(fyne.NewPos(0, r.scroll.size.Height))
  224. }
  225. if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth {
  226. r.horizArea.Resize(fyne.NewSize(size.Width, r.horizArea.MinSize().Height))
  227. r.horizArea.Move(fyne.NewPos(0, r.scroll.Size().Height-r.horizArea.Size().Height))
  228. r.leftShadow.Resize(fyne.NewSize(0, size.Height))
  229. r.rightShadow.Resize(fyne.NewSize(0, size.Height))
  230. r.rightShadow.Move(fyne.NewPos(r.scroll.size.Width, 0))
  231. }
  232. r.updatePosition()
  233. }
  234. func (r *scrollContainerRenderer) Layout(size fyne.Size) {
  235. c := r.scroll.Content
  236. c.Resize(c.MinSize().Max(size))
  237. r.layoutBars(size)
  238. }
  239. func (r *scrollContainerRenderer) MinSize() fyne.Size {
  240. return r.scroll.MinSize()
  241. }
  242. func (r *scrollContainerRenderer) Refresh() {
  243. if len(r.BaseRenderer.Objects()) == 0 || r.BaseRenderer.Objects()[0] != r.scroll.Content {
  244. // push updated content object to baseRenderer
  245. r.BaseRenderer.Objects()[0] = r.scroll.Content
  246. }
  247. if r.oldMinSize == r.scroll.Content.MinSize() && r.oldMinSize == r.scroll.Content.Size() &&
  248. (r.scroll.Size().Width <= r.oldMinSize.Width && r.scroll.Size().Height <= r.oldMinSize.Height) {
  249. r.layoutBars(r.scroll.Size())
  250. return
  251. }
  252. r.oldMinSize = r.scroll.Content.MinSize()
  253. r.Layout(r.scroll.Size())
  254. }
  255. func (r *scrollContainerRenderer) handleAreaVisibility(contentSize, scrollSize float32, area *scrollBarArea) {
  256. if contentSize <= scrollSize {
  257. area.Hide()
  258. } else if r.scroll.Visible() {
  259. area.Show()
  260. }
  261. }
  262. func (r *scrollContainerRenderer) handleShadowVisibility(offset, contentSize, scrollSize float32, shadowStart fyne.CanvasObject, shadowEnd fyne.CanvasObject) {
  263. if !r.scroll.Visible() {
  264. return
  265. }
  266. if offset > 0 {
  267. shadowStart.Show()
  268. } else {
  269. shadowStart.Hide()
  270. }
  271. if offset < contentSize-scrollSize {
  272. shadowEnd.Show()
  273. } else {
  274. shadowEnd.Hide()
  275. }
  276. }
  277. func (r *scrollContainerRenderer) updatePosition() {
  278. if r.scroll.Content == nil {
  279. return
  280. }
  281. scrollSize := r.scroll.Size()
  282. contentSize := r.scroll.Content.Size()
  283. r.scroll.Content.Move(fyne.NewPos(-r.scroll.Offset.X, -r.scroll.Offset.Y))
  284. if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth {
  285. r.handleAreaVisibility(contentSize.Height, scrollSize.Height, r.vertArea)
  286. r.handleShadowVisibility(r.scroll.Offset.Y, contentSize.Height, scrollSize.Height, r.topShadow, r.bottomShadow)
  287. cache.Renderer(r.vertArea).Layout(r.scroll.size)
  288. } else {
  289. r.vertArea.Hide()
  290. r.topShadow.Hide()
  291. r.bottomShadow.Hide()
  292. }
  293. if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth {
  294. r.handleAreaVisibility(contentSize.Width, scrollSize.Width, r.horizArea)
  295. r.handleShadowVisibility(r.scroll.Offset.X, contentSize.Width, scrollSize.Width, r.leftShadow, r.rightShadow)
  296. cache.Renderer(r.horizArea).Layout(r.scroll.size)
  297. } else {
  298. r.horizArea.Hide()
  299. r.leftShadow.Hide()
  300. r.rightShadow.Hide()
  301. }
  302. if r.scroll.Direction != ScrollHorizontalOnly {
  303. canvas.Refresh(r.vertArea) // this is required to force the canvas to update, we have no "Redraw()"
  304. } else {
  305. canvas.Refresh(r.horizArea) // this is required like above but if we are horizontal
  306. }
  307. }
  308. // Scroll defines a container that is smaller than the Content.
  309. // The Offset is used to determine the position of the child widgets within the container.
  310. type Scroll struct {
  311. Base
  312. minSize fyne.Size
  313. Direction ScrollDirection
  314. Content fyne.CanvasObject
  315. Offset fyne.Position
  316. // OnScrolled can be set to be notified when the Scroll has changed position.
  317. // You should not update the Scroll.Offset from this method.
  318. //
  319. // Since: 2.0
  320. OnScrolled func(fyne.Position)
  321. }
  322. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  323. func (s *Scroll) CreateRenderer() fyne.WidgetRenderer {
  324. scr := &scrollContainerRenderer{
  325. BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{s.Content}),
  326. scroll: s,
  327. }
  328. scr.vertArea = newScrollBarArea(s, scrollBarOrientationVertical)
  329. scr.topShadow = NewShadow(ShadowBottom, SubmergedContentLevel)
  330. scr.bottomShadow = NewShadow(ShadowTop, SubmergedContentLevel)
  331. scr.horizArea = newScrollBarArea(s, scrollBarOrientationHorizontal)
  332. scr.leftShadow = NewShadow(ShadowRight, SubmergedContentLevel)
  333. scr.rightShadow = NewShadow(ShadowLeft, SubmergedContentLevel)
  334. scr.SetObjects(append(scr.Objects(), scr.topShadow, scr.bottomShadow, scr.leftShadow, scr.rightShadow,
  335. scr.vertArea, scr.horizArea))
  336. scr.updatePosition()
  337. return scr
  338. }
  339. // ScrollToBottom will scroll content to container bottom - to show latest info which end user just added
  340. func (s *Scroll) ScrollToBottom() {
  341. s.scrollBy(0, -1*(s.Content.MinSize().Height-s.Size().Height-s.Offset.Y))
  342. s.Refresh()
  343. }
  344. // ScrollToTop will scroll content to container top
  345. func (s *Scroll) ScrollToTop() {
  346. s.scrollBy(0, -s.Offset.Y)
  347. }
  348. // DragEnd will stop scrolling on mobile has stopped
  349. func (s *Scroll) DragEnd() {
  350. }
  351. // Dragged will scroll on any drag - bar or otherwise - for mobile
  352. func (s *Scroll) Dragged(e *fyne.DragEvent) {
  353. if !fyne.CurrentDevice().IsMobile() {
  354. return
  355. }
  356. if s.updateOffset(e.Dragged.DX, e.Dragged.DY) {
  357. s.refreshWithoutOffsetUpdate()
  358. }
  359. }
  360. // MinSize returns the smallest size this widget can shrink to
  361. func (s *Scroll) MinSize() fyne.Size {
  362. min := fyne.NewSize(scrollContainerMinSize, scrollContainerMinSize).Max(s.minSize)
  363. switch s.Direction {
  364. case ScrollHorizontalOnly:
  365. min.Height = fyne.Max(min.Height, s.Content.MinSize().Height)
  366. case ScrollVerticalOnly:
  367. min.Width = fyne.Max(min.Width, s.Content.MinSize().Width)
  368. case ScrollNone:
  369. return s.Content.MinSize()
  370. }
  371. return min
  372. }
  373. // SetMinSize specifies a minimum size for this scroll container.
  374. // If the specified size is larger than the content size then scrolling will not be enabled
  375. // This can be helpful to appear larger than default if the layout is collapsing this widget.
  376. func (s *Scroll) SetMinSize(size fyne.Size) {
  377. s.minSize = size
  378. }
  379. // Refresh causes this widget to be redrawn in it's current state
  380. func (s *Scroll) Refresh() {
  381. s.updateOffset(0, 0)
  382. s.refreshWithoutOffsetUpdate()
  383. }
  384. // Resize is called when this scroller should change size. We refresh to ensure the scroll bars are updated.
  385. func (s *Scroll) Resize(sz fyne.Size) {
  386. if sz == s.size {
  387. return
  388. }
  389. s.Base.Resize(sz)
  390. s.Refresh()
  391. }
  392. func (s *Scroll) refreshWithoutOffsetUpdate() {
  393. s.Base.Refresh()
  394. }
  395. // Scrolled is called when an input device triggers a scroll event
  396. func (s *Scroll) Scrolled(ev *fyne.ScrollEvent) {
  397. s.scrollBy(ev.Scrolled.DX, ev.Scrolled.DY)
  398. }
  399. func (s *Scroll) scrollBy(dx, dy float32) {
  400. if s.Size().Width < s.Content.MinSize().Width && s.Size().Height >= s.Content.MinSize().Height && dx == 0 {
  401. dx, dy = dy, dx
  402. }
  403. if s.updateOffset(dx, dy) {
  404. s.refreshWithoutOffsetUpdate()
  405. }
  406. }
  407. func (s *Scroll) updateOffset(deltaX, deltaY float32) bool {
  408. if s.Content.Size().Width <= s.Size().Width && s.Content.Size().Height <= s.Size().Height {
  409. if s.Offset.X != 0 || s.Offset.Y != 0 {
  410. s.Offset.X = 0
  411. s.Offset.Y = 0
  412. return true
  413. }
  414. return false
  415. }
  416. oldX := s.Offset.X
  417. oldY := s.Offset.Y
  418. s.Offset.X = computeOffset(s.Offset.X, -deltaX, s.Size().Width, s.Content.MinSize().Width)
  419. s.Offset.Y = computeOffset(s.Offset.Y, -deltaY, s.Size().Height, s.Content.MinSize().Height)
  420. if f := s.OnScrolled; f != nil && (s.Offset.X != oldX || s.Offset.Y != oldY) {
  421. f(s.Offset)
  422. }
  423. return true
  424. }
  425. func computeOffset(start, delta, outerWidth, innerWidth float32) float32 {
  426. offset := start + delta
  427. if offset+outerWidth >= innerWidth {
  428. offset = innerWidth - outerWidth
  429. }
  430. if offset < 0 {
  431. offset = 0
  432. }
  433. return offset
  434. }
  435. // NewScroll creates a scrollable parent wrapping the specified content.
  436. // Note that this may cause the MinSize to be smaller than that of the passed object.
  437. func NewScroll(content fyne.CanvasObject) *Scroll {
  438. s := newScrollContainerWithDirection(ScrollBoth, content)
  439. s.ExtendBaseWidget(s)
  440. return s
  441. }
  442. // NewHScroll create a scrollable parent wrapping the specified content.
  443. // Note that this may cause the MinSize.Width to be smaller than that of the passed object.
  444. func NewHScroll(content fyne.CanvasObject) *Scroll {
  445. s := newScrollContainerWithDirection(ScrollHorizontalOnly, content)
  446. s.ExtendBaseWidget(s)
  447. return s
  448. }
  449. // NewVScroll create a scrollable parent wrapping the specified content.
  450. // Note that this may cause the MinSize.Height to be smaller than that of the passed object.
  451. func NewVScroll(content fyne.CanvasObject) *Scroll {
  452. s := newScrollContainerWithDirection(ScrollVerticalOnly, content)
  453. s.ExtendBaseWidget(s)
  454. return s
  455. }
  456. func newScrollContainerWithDirection(direction ScrollDirection, content fyne.CanvasObject) *Scroll {
  457. s := &Scroll{
  458. Direction: direction,
  459. Content: content,
  460. }
  461. s.ExtendBaseWidget(s)
  462. return s
  463. }