list.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. package widget
  2. import (
  3. "fmt"
  4. "math"
  5. "sync"
  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. // ListItemID uniquely identifies an item within a list.
  14. type ListItemID = int
  15. // Declare conformity with Widget interface.
  16. var _ fyne.Widget = (*List)(nil)
  17. // List is a widget that pools list items for performance and
  18. // lays the items out in a vertical direction inside of a scroller.
  19. // List requires that all items are the same size.
  20. //
  21. // Since: 1.4
  22. type List struct {
  23. BaseWidget
  24. Length func() int `json:"-"`
  25. CreateItem func() fyne.CanvasObject `json:"-"`
  26. UpdateItem func(id ListItemID, item fyne.CanvasObject) `json:"-"`
  27. OnSelected func(id ListItemID) `json:"-"`
  28. OnUnselected func(id ListItemID) `json:"-"`
  29. scroller *widget.Scroll
  30. selected []ListItemID
  31. itemMin fyne.Size
  32. itemHeights map[ListItemID]float32
  33. offsetY float32
  34. offsetUpdated func(fyne.Position)
  35. }
  36. // NewList creates and returns a list widget for displaying items in
  37. // a vertical layout with scrolling and caching for performance.
  38. //
  39. // Since: 1.4
  40. func NewList(length func() int, createItem func() fyne.CanvasObject, updateItem func(ListItemID, fyne.CanvasObject)) *List {
  41. list := &List{BaseWidget: BaseWidget{}, Length: length, CreateItem: createItem, UpdateItem: updateItem}
  42. list.ExtendBaseWidget(list)
  43. return list
  44. }
  45. // NewListWithData creates a new list widget that will display the contents of the provided data.
  46. //
  47. // Since: 2.0
  48. func NewListWithData(data binding.DataList, createItem func() fyne.CanvasObject, updateItem func(binding.DataItem, fyne.CanvasObject)) *List {
  49. l := NewList(
  50. data.Length,
  51. createItem,
  52. func(i ListItemID, o fyne.CanvasObject) {
  53. item, err := data.GetItem(i)
  54. if err != nil {
  55. fyne.LogError(fmt.Sprintf("Error getting data item %d", i), err)
  56. return
  57. }
  58. updateItem(item, o)
  59. })
  60. data.AddListener(binding.NewDataListener(l.Refresh))
  61. return l
  62. }
  63. // CreateRenderer is a private method to Fyne which links this widget to its renderer.
  64. func (l *List) CreateRenderer() fyne.WidgetRenderer {
  65. l.ExtendBaseWidget(l)
  66. if f := l.CreateItem; f != nil {
  67. if l.itemMin.IsZero() {
  68. l.itemMin = newListItem(f(), nil).MinSize()
  69. }
  70. }
  71. layout := &fyne.Container{}
  72. l.scroller = widget.NewVScroll(layout)
  73. layout.Layout = newListLayout(l)
  74. layout.Resize(layout.MinSize())
  75. objects := []fyne.CanvasObject{l.scroller}
  76. lr := newListRenderer(objects, l, l.scroller, layout)
  77. return lr
  78. }
  79. // MinSize returns the size that this widget should not shrink below.
  80. func (l *List) MinSize() fyne.Size {
  81. l.ExtendBaseWidget(l)
  82. return l.BaseWidget.MinSize()
  83. }
  84. // SetItemHeight supports changing the height of the specified list item. Items normally take the height of the template
  85. // returned from the CreateItem callback. The height parameter uses the same units as a fyne.Size type and refers
  86. // to the internal content height not including the divider size.
  87. //
  88. // Since: 2.3
  89. func (l *List) SetItemHeight(id ListItemID, height float32) {
  90. l.propertyLock.Lock()
  91. if l.itemHeights == nil {
  92. l.itemHeights = make(map[ListItemID]float32)
  93. }
  94. refresh := l.itemHeights[id] != height
  95. l.itemHeights[id] = height
  96. l.propertyLock.Unlock()
  97. if refresh {
  98. l.Refresh()
  99. }
  100. }
  101. func (l *List) scrollTo(id ListItemID) {
  102. if l.scroller == nil {
  103. return
  104. }
  105. separatorThickness := theme.Padding()
  106. y := float32(0)
  107. if l.itemHeights == nil || len(l.itemHeights) == 0 {
  108. y = (float32(id) * l.itemMin.Height) + (float32(id) * separatorThickness)
  109. } else {
  110. for i := 0; i < id; i++ {
  111. height := l.itemMin.Height
  112. if h, ok := l.itemHeights[i]; ok {
  113. height = h
  114. }
  115. y += height + separatorThickness
  116. }
  117. }
  118. if y < l.scroller.Offset.Y {
  119. l.scroller.Offset.Y = y
  120. } else if y+l.itemMin.Height > l.scroller.Offset.Y+l.scroller.Size().Height {
  121. l.scroller.Offset.Y = y + l.itemMin.Height - l.scroller.Size().Height
  122. }
  123. l.offsetUpdated(l.scroller.Offset)
  124. }
  125. // Resize is called when this list should change size. We refresh to ensure invisible items are drawn.
  126. func (l *List) Resize(s fyne.Size) {
  127. l.BaseWidget.Resize(s)
  128. if l.scroller == nil {
  129. return
  130. }
  131. l.offsetUpdated(l.scroller.Offset)
  132. l.scroller.Content.(*fyne.Container).Layout.(*listLayout).updateList(true)
  133. }
  134. // Select add the item identified by the given ID to the selection.
  135. func (l *List) Select(id ListItemID) {
  136. if len(l.selected) > 0 && id == l.selected[0] {
  137. return
  138. }
  139. length := 0
  140. if f := l.Length; f != nil {
  141. length = f()
  142. }
  143. if id < 0 || id >= length {
  144. return
  145. }
  146. old := l.selected
  147. l.selected = []ListItemID{id}
  148. defer func() {
  149. if f := l.OnUnselected; f != nil && len(old) > 0 {
  150. f(old[0])
  151. }
  152. if f := l.OnSelected; f != nil {
  153. f(id)
  154. }
  155. }()
  156. l.scrollTo(id)
  157. l.Refresh()
  158. }
  159. // ScrollTo scrolls to the item represented by id
  160. //
  161. // Since: 2.1
  162. func (l *List) ScrollTo(id ListItemID) {
  163. length := 0
  164. if f := l.Length; f != nil {
  165. length = f()
  166. }
  167. if id < 0 || id >= length {
  168. return
  169. }
  170. l.scrollTo(id)
  171. l.Refresh()
  172. }
  173. // ScrollToBottom scrolls to the end of the list
  174. //
  175. // Since: 2.1
  176. func (l *List) ScrollToBottom() {
  177. length := 0
  178. if f := l.Length; f != nil {
  179. length = f()
  180. }
  181. if length > 0 {
  182. length--
  183. }
  184. l.scrollTo(length)
  185. l.Refresh()
  186. }
  187. // ScrollToTop scrolls to the start of the list
  188. //
  189. // Since: 2.1
  190. func (l *List) ScrollToTop() {
  191. l.scrollTo(0)
  192. l.Refresh()
  193. }
  194. // Unselect removes the item identified by the given ID from the selection.
  195. func (l *List) Unselect(id ListItemID) {
  196. if len(l.selected) == 0 || l.selected[0] != id {
  197. return
  198. }
  199. l.selected = nil
  200. l.Refresh()
  201. if f := l.OnUnselected; f != nil {
  202. f(id)
  203. }
  204. }
  205. // UnselectAll removes all items from the selection.
  206. //
  207. // Since: 2.1
  208. func (l *List) UnselectAll() {
  209. if len(l.selected) == 0 {
  210. return
  211. }
  212. selected := l.selected
  213. l.selected = nil
  214. l.Refresh()
  215. if f := l.OnUnselected; f != nil {
  216. for _, id := range selected {
  217. f(id)
  218. }
  219. }
  220. }
  221. func (l *List) visibleItemHeights(itemHeight float32, length int) (visible []float32, offY float32, minRow int) {
  222. rowOffset := float32(0)
  223. isVisible := false
  224. visible = []float32{}
  225. if l.scroller.Size().Height <= 0 {
  226. return
  227. }
  228. // theme.Padding is a slow call, so we cache it
  229. padding := theme.Padding()
  230. if len(l.itemHeights) == 0 {
  231. paddedItemHeight := itemHeight + padding
  232. offY = float32(math.Floor(float64(l.offsetY/paddedItemHeight))) * paddedItemHeight
  233. minRow = int(math.Floor(float64(offY / paddedItemHeight)))
  234. maxRow := int(math.Ceil(float64((offY + l.scroller.Size().Height) / paddedItemHeight)))
  235. if minRow > length-1 {
  236. minRow = length - 1
  237. }
  238. if minRow < 0 {
  239. minRow = 0
  240. offY = 0
  241. }
  242. if maxRow > length {
  243. maxRow = length
  244. }
  245. visible = make([]float32, maxRow-minRow)
  246. for i := 0; i < maxRow-minRow; i++ {
  247. visible[i] = itemHeight
  248. }
  249. return
  250. }
  251. for i := 0; i < length; i++ {
  252. height := itemHeight
  253. if h, ok := l.itemHeights[i]; ok {
  254. height = h
  255. }
  256. if rowOffset <= l.offsetY-height-padding {
  257. // before scroll
  258. } else if rowOffset <= l.offsetY {
  259. minRow = i
  260. offY = rowOffset
  261. isVisible = true
  262. }
  263. if rowOffset >= l.offsetY+l.scroller.Size().Height {
  264. break
  265. }
  266. rowOffset += height + padding
  267. if isVisible {
  268. visible = append(visible, height)
  269. }
  270. }
  271. return
  272. }
  273. // Declare conformity with WidgetRenderer interface.
  274. var _ fyne.WidgetRenderer = (*listRenderer)(nil)
  275. type listRenderer struct {
  276. widget.BaseRenderer
  277. list *List
  278. scroller *widget.Scroll
  279. layout *fyne.Container
  280. }
  281. func newListRenderer(objects []fyne.CanvasObject, l *List, scroller *widget.Scroll, layout *fyne.Container) *listRenderer {
  282. lr := &listRenderer{BaseRenderer: widget.NewBaseRenderer(objects), list: l, scroller: scroller, layout: layout}
  283. lr.scroller.OnScrolled = l.offsetUpdated
  284. return lr
  285. }
  286. func (l *listRenderer) Layout(size fyne.Size) {
  287. l.scroller.Resize(size)
  288. }
  289. func (l *listRenderer) MinSize() fyne.Size {
  290. return l.scroller.MinSize().Max(l.list.itemMin)
  291. }
  292. func (l *listRenderer) Refresh() {
  293. if f := l.list.CreateItem; f != nil {
  294. l.list.itemMin = newListItem(f(), nil).MinSize()
  295. }
  296. l.Layout(l.list.Size())
  297. l.scroller.Refresh()
  298. l.layout.Layout.(*listLayout).updateList(true)
  299. canvas.Refresh(l.list.super())
  300. }
  301. // Declare conformity with interfaces.
  302. var _ fyne.Focusable = (*listItem)(nil)
  303. var _ fyne.Widget = (*listItem)(nil)
  304. var _ fyne.Tappable = (*listItem)(nil)
  305. var _ desktop.Hoverable = (*listItem)(nil)
  306. type listItem struct {
  307. BaseWidget
  308. onTapped func()
  309. background *canvas.Rectangle
  310. child fyne.CanvasObject
  311. hovered, selected bool
  312. }
  313. func newListItem(child fyne.CanvasObject, tapped func()) *listItem {
  314. li := &listItem{
  315. child: child,
  316. onTapped: tapped,
  317. }
  318. li.ExtendBaseWidget(li)
  319. return li
  320. }
  321. // CreateRenderer is a private method to Fyne which links this widget to its renderer.
  322. func (li *listItem) CreateRenderer() fyne.WidgetRenderer {
  323. li.ExtendBaseWidget(li)
  324. li.background = canvas.NewRectangle(theme.HoverColor())
  325. li.background.Hide()
  326. objects := []fyne.CanvasObject{li.background, li.child}
  327. return &listItemRenderer{widget.NewBaseRenderer(objects), li}
  328. }
  329. // FocusGained is called after this listItem has gained focus.
  330. //
  331. // Implements: fyne.Focusable
  332. func (li *listItem) FocusGained() {
  333. li.hovered = true
  334. li.Refresh()
  335. }
  336. // FocusLost is called after this listItem has lost focus.
  337. //
  338. // Implements: fyne.Focusable
  339. func (li *listItem) FocusLost() {
  340. li.hovered = false
  341. li.Refresh()
  342. }
  343. // MinSize returns the size that this widget should not shrink below.
  344. func (li *listItem) MinSize() fyne.Size {
  345. li.ExtendBaseWidget(li)
  346. return li.BaseWidget.MinSize()
  347. }
  348. // MouseIn is called when a desktop pointer enters the widget.
  349. func (li *listItem) MouseIn(*desktop.MouseEvent) {
  350. li.hovered = true
  351. li.Refresh()
  352. }
  353. // MouseMoved is called when a desktop pointer hovers over the widget.
  354. func (li *listItem) MouseMoved(*desktop.MouseEvent) {
  355. }
  356. // MouseOut is called when a desktop pointer exits the widget.
  357. func (li *listItem) MouseOut() {
  358. li.hovered = false
  359. li.Refresh()
  360. }
  361. // Tapped is called when a pointer tapped event is captured and triggers any tap handler.
  362. func (li *listItem) Tapped(*fyne.PointEvent) {
  363. if li.onTapped != nil {
  364. li.selected = true
  365. li.Refresh()
  366. li.onTapped()
  367. }
  368. }
  369. // TypedKey is called if a key event happens while this listItem is focused.
  370. //
  371. // Implements: fyne.Focusable
  372. func (li *listItem) TypedKey(event *fyne.KeyEvent) {
  373. switch event.Name {
  374. case fyne.KeySpace:
  375. li.selected = true
  376. li.Refresh()
  377. if li.onTapped != nil {
  378. li.onTapped()
  379. }
  380. }
  381. }
  382. // TypedRune is called if a text event happens while this listItem is focused.
  383. //
  384. // Implements: fyne.Focusable
  385. func (li *listItem) TypedRune(_ rune) {
  386. // intentionally left blank
  387. }
  388. // Declare conformity with the WidgetRenderer interface.
  389. var _ fyne.WidgetRenderer = (*listItemRenderer)(nil)
  390. type listItemRenderer struct {
  391. widget.BaseRenderer
  392. item *listItem
  393. }
  394. // MinSize calculates the minimum size of a listItem.
  395. // This is based on the size of the status indicator and the size of the child object.
  396. func (li *listItemRenderer) MinSize() fyne.Size {
  397. return li.item.child.MinSize()
  398. }
  399. // Layout the components of the listItem widget.
  400. func (li *listItemRenderer) Layout(size fyne.Size) {
  401. li.item.background.Resize(size)
  402. li.item.child.Resize(size)
  403. }
  404. func (li *listItemRenderer) Refresh() {
  405. if li.item.selected {
  406. li.item.background.FillColor = theme.SelectionColor()
  407. li.item.background.Show()
  408. } else if li.item.hovered {
  409. li.item.background.FillColor = theme.HoverColor()
  410. li.item.background.Show()
  411. } else {
  412. li.item.background.Hide()
  413. }
  414. li.item.background.Refresh()
  415. canvas.Refresh(li.item.super())
  416. }
  417. // Declare conformity with Layout interface.
  418. var _ fyne.Layout = (*listLayout)(nil)
  419. type listLayout struct {
  420. list *List
  421. separators []fyne.CanvasObject
  422. children []fyne.CanvasObject
  423. itemPool *syncPool
  424. visible map[ListItemID]*listItem
  425. renderLock sync.Mutex
  426. }
  427. func newListLayout(list *List) fyne.Layout {
  428. l := &listLayout{list: list, itemPool: &syncPool{}, visible: make(map[ListItemID]*listItem)}
  429. list.offsetUpdated = l.offsetUpdated
  430. return l
  431. }
  432. func (l *listLayout) Layout([]fyne.CanvasObject, fyne.Size) {
  433. l.updateList(true)
  434. }
  435. func (l *listLayout) MinSize([]fyne.CanvasObject) fyne.Size {
  436. l.list.propertyLock.Lock()
  437. defer l.list.propertyLock.Unlock()
  438. items := 0
  439. if f := l.list.Length; f == nil {
  440. return fyne.NewSize(0, 0)
  441. } else {
  442. items = f()
  443. }
  444. separatorThickness := theme.Padding()
  445. if l.list.itemHeights == nil || len(l.list.itemHeights) == 0 {
  446. return fyne.NewSize(l.list.itemMin.Width,
  447. (l.list.itemMin.Height+separatorThickness)*float32(items)-separatorThickness)
  448. }
  449. height := float32(0)
  450. templateHeight := l.list.itemMin.Height
  451. for item := 0; item < items; item++ {
  452. itemHeight, ok := l.list.itemHeights[item]
  453. if ok {
  454. height += itemHeight
  455. } else {
  456. height += templateHeight
  457. }
  458. }
  459. return fyne.NewSize(l.list.itemMin.Width, height+separatorThickness*float32(items-1))
  460. }
  461. func (l *listLayout) getItem() *listItem {
  462. item := l.itemPool.Obtain()
  463. if item == nil {
  464. if f := l.list.CreateItem; f != nil {
  465. item = newListItem(f(), nil)
  466. }
  467. }
  468. return item.(*listItem)
  469. }
  470. func (l *listLayout) offsetUpdated(pos fyne.Position) {
  471. if l.list.offsetY == pos.Y {
  472. return
  473. }
  474. l.list.offsetY = pos.Y
  475. l.updateList(false)
  476. }
  477. func (l *listLayout) setupListItem(li *listItem, id ListItemID, focus bool) {
  478. previousIndicator := li.selected
  479. li.selected = false
  480. for _, s := range l.list.selected {
  481. if id == s {
  482. li.selected = true
  483. break
  484. }
  485. }
  486. if focus {
  487. li.hovered = true
  488. li.Refresh()
  489. } else if previousIndicator != li.selected || li.hovered {
  490. li.hovered = false
  491. li.Refresh()
  492. }
  493. if f := l.list.UpdateItem; f != nil {
  494. f(id, li.child)
  495. }
  496. li.onTapped = func() {
  497. l.list.Select(id)
  498. }
  499. }
  500. func (l *listLayout) updateList(refresh bool) {
  501. l.renderLock.Lock()
  502. separatorThickness := theme.Padding()
  503. width := l.list.Size().Width
  504. length := 0
  505. if f := l.list.Length; f != nil {
  506. length = f()
  507. }
  508. if l.list.UpdateItem == nil {
  509. fyne.LogError("Missing UpdateCell callback required for List", nil)
  510. }
  511. wasVisible := l.visible
  512. l.list.propertyLock.Lock()
  513. visibleRowHeights, offY, minRow := l.list.visibleItemHeights(l.list.itemMin.Height, length)
  514. l.list.propertyLock.Unlock()
  515. if len(visibleRowHeights) == 0 && length > 0 { // we can't show anything until we have some dimensions
  516. l.renderLock.Unlock() // user code should not be locked
  517. return
  518. }
  519. visible := make(map[ListItemID]*listItem, len(visibleRowHeights))
  520. cells := make([]fyne.CanvasObject, len(visibleRowHeights))
  521. y := offY
  522. for index, itemHeight := range visibleRowHeights {
  523. row := index + minRow
  524. size := fyne.NewSize(width, itemHeight)
  525. c, ok := wasVisible[row]
  526. if !ok {
  527. c = l.getItem()
  528. if c == nil {
  529. continue
  530. }
  531. c.Resize(size)
  532. }
  533. c.Move(fyne.NewPos(0, y))
  534. c.Resize(size)
  535. y += itemHeight + separatorThickness
  536. visible[row] = c
  537. cells[index] = c
  538. }
  539. l.visible = visible
  540. var focused fyne.Focusable
  541. canvas := fyne.CurrentApp().Driver().CanvasForObject(l.list)
  542. if canvas != nil {
  543. focused = canvas.Focused()
  544. }
  545. for id, old := range wasVisible {
  546. if _, ok := l.visible[id]; !ok {
  547. if focused == old {
  548. canvas.Focus(nil)
  549. }
  550. l.itemPool.Release(old)
  551. }
  552. }
  553. l.children = cells
  554. l.updateSeparators()
  555. objects := l.children
  556. objects = append(objects, l.separators...)
  557. l.list.scroller.Content.(*fyne.Container).Objects = objects
  558. l.renderLock.Unlock() // user code should not be locked
  559. for row, obj := range visible {
  560. l.setupListItem(obj, row, focused == obj)
  561. }
  562. }
  563. func (l *listLayout) updateSeparators() {
  564. if len(l.children) > 1 {
  565. if len(l.separators) > len(l.children) {
  566. l.separators = l.separators[:len(l.children)]
  567. } else {
  568. for i := len(l.separators); i < len(l.children); i++ {
  569. l.separators = append(l.separators, NewSeparator())
  570. }
  571. }
  572. } else {
  573. l.separators = nil
  574. }
  575. separatorThickness := theme.SeparatorThicknessSize()
  576. dividerOff := (theme.Padding() + separatorThickness) / 2
  577. for i, child := range l.children {
  578. if i == 0 {
  579. continue
  580. }
  581. l.separators[i].Move(fyne.NewPos(0, child.Position().Y-dividerOff))
  582. l.separators[i].Resize(fyne.NewSize(l.list.Size().Width, separatorThickness))
  583. l.separators[i].Show()
  584. }
  585. }