popup.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. package widget
  2. import (
  3. "fyne.io/fyne/v2"
  4. "fyne.io/fyne/v2/canvas"
  5. "fyne.io/fyne/v2/internal/widget"
  6. "fyne.io/fyne/v2/theme"
  7. )
  8. // PopUp is a widget that can float above the user interface.
  9. // It wraps any standard elements with padding and a shadow.
  10. // If it is modal then the shadow will cover the entire canvas it hovers over and block interactions.
  11. type PopUp struct {
  12. BaseWidget
  13. Content fyne.CanvasObject
  14. Canvas fyne.Canvas
  15. innerPos fyne.Position
  16. innerSize fyne.Size
  17. modal bool
  18. overlayShown bool
  19. }
  20. // Hide this widget, if it was previously visible
  21. func (p *PopUp) Hide() {
  22. if p.overlayShown {
  23. p.Canvas.Overlays().Remove(p)
  24. p.overlayShown = false
  25. }
  26. p.BaseWidget.Hide()
  27. }
  28. // Move the widget to a new position. A PopUp position is absolute to the top, left of its canvas.
  29. // For PopUp this actually moves the content so checking Position() will not return the same value as is set here.
  30. func (p *PopUp) Move(pos fyne.Position) {
  31. if p.modal {
  32. return
  33. }
  34. p.innerPos = pos
  35. p.Refresh()
  36. }
  37. // Resize changes the size of the PopUp's content.
  38. // PopUps always have the size of their canvas, but this call updates the
  39. // size of the content portion.
  40. //
  41. // Implements: fyne.Widget
  42. func (p *PopUp) Resize(size fyne.Size) {
  43. p.innerSize = size
  44. // The canvas size might not have changed and therefore the Resize won't trigger a layout.
  45. // Until we have a widget.Relayout() or similar, the renderer's refresh will do the re-layout.
  46. p.Refresh()
  47. }
  48. // Show this pop-up as overlay if not already shown.
  49. func (p *PopUp) Show() {
  50. if !p.overlayShown {
  51. p.Canvas.Overlays().Add(p)
  52. p.overlayShown = true
  53. }
  54. p.Refresh()
  55. p.BaseWidget.Show()
  56. }
  57. // ShowAtPosition shows this pop-up at the given position.
  58. func (p *PopUp) ShowAtPosition(pos fyne.Position) {
  59. p.Move(pos)
  60. p.Show()
  61. }
  62. // Tapped is called when the user taps the popUp background - if not modal then dismiss this widget
  63. func (p *PopUp) Tapped(_ *fyne.PointEvent) {
  64. if !p.modal {
  65. p.Hide()
  66. }
  67. }
  68. // TappedSecondary is called when the user right/alt taps the background - if not modal then dismiss this widget
  69. func (p *PopUp) TappedSecondary(_ *fyne.PointEvent) {
  70. if !p.modal {
  71. p.Hide()
  72. }
  73. }
  74. // MinSize returns the size that this widget should not shrink below
  75. func (p *PopUp) MinSize() fyne.Size {
  76. p.ExtendBaseWidget(p)
  77. return p.BaseWidget.MinSize()
  78. }
  79. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  80. func (p *PopUp) CreateRenderer() fyne.WidgetRenderer {
  81. p.ExtendBaseWidget(p)
  82. background := canvas.NewRectangle(theme.OverlayBackgroundColor())
  83. if p.modal {
  84. underlay := canvas.NewRectangle(theme.ShadowColor())
  85. objects := []fyne.CanvasObject{underlay, background, p.Content}
  86. return &modalPopUpRenderer{
  87. widget.NewShadowingRenderer(objects, widget.DialogLevel),
  88. popUpBaseRenderer{popUp: p, background: background},
  89. underlay,
  90. }
  91. }
  92. objects := []fyne.CanvasObject{background, p.Content}
  93. return &popUpRenderer{
  94. widget.NewShadowingRenderer(objects, widget.PopUpLevel),
  95. popUpBaseRenderer{popUp: p, background: background},
  96. }
  97. }
  98. // ShowPopUpAtPosition creates a new popUp for the specified content at the specified absolute position.
  99. // It will then display the popup on the passed canvas.
  100. func ShowPopUpAtPosition(content fyne.CanvasObject, canvas fyne.Canvas, pos fyne.Position) {
  101. newPopUp(content, canvas).ShowAtPosition(pos)
  102. }
  103. func newPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp {
  104. ret := &PopUp{Content: content, Canvas: canvas, modal: false}
  105. ret.ExtendBaseWidget(ret)
  106. return ret
  107. }
  108. // NewPopUp creates a new popUp for the specified content and displays it on the passed canvas.
  109. func NewPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp {
  110. return newPopUp(content, canvas)
  111. }
  112. // ShowPopUp creates a new popUp for the specified content and displays it on the passed canvas.
  113. func ShowPopUp(content fyne.CanvasObject, canvas fyne.Canvas) {
  114. newPopUp(content, canvas).Show()
  115. }
  116. func newModalPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp {
  117. p := &PopUp{Content: content, Canvas: canvas, modal: true}
  118. p.ExtendBaseWidget(p)
  119. return p
  120. }
  121. // NewModalPopUp creates a new popUp for the specified content and displays it on the passed canvas.
  122. // A modal PopUp blocks interactions with underlying elements, covered with a semi-transparent overlay.
  123. func NewModalPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp {
  124. return newModalPopUp(content, canvas)
  125. }
  126. // ShowModalPopUp creates a new popUp for the specified content and displays it on the passed canvas.
  127. // A modal PopUp blocks interactions with underlying elements, covered with a semi-transparent overlay.
  128. func ShowModalPopUp(content fyne.CanvasObject, canvas fyne.Canvas) {
  129. p := newModalPopUp(content, canvas)
  130. p.Show()
  131. }
  132. type popUpBaseRenderer struct {
  133. popUp *PopUp
  134. background *canvas.Rectangle
  135. }
  136. func (r *popUpBaseRenderer) padding() fyne.Size {
  137. return fyne.NewSize(theme.InnerPadding(), theme.InnerPadding())
  138. }
  139. func (r *popUpBaseRenderer) offset() fyne.Position {
  140. return fyne.NewPos(theme.InnerPadding()/2, theme.InnerPadding()/2)
  141. }
  142. type popUpRenderer struct {
  143. *widget.ShadowingRenderer
  144. popUpBaseRenderer
  145. }
  146. func (r *popUpRenderer) Layout(_ fyne.Size) {
  147. innerSize := r.popUp.innerSize.Max(r.popUp.MinSize())
  148. r.popUp.Content.Resize(innerSize.Subtract(r.padding()))
  149. innerPos := r.popUp.innerPos
  150. if innerPos.X+innerSize.Width > r.popUp.Canvas.Size().Width {
  151. innerPos.X = r.popUp.Canvas.Size().Width - innerSize.Width
  152. if innerPos.X < 0 {
  153. innerPos.X = 0 // TODO here we may need a scroller as it's wider than our canvas
  154. }
  155. }
  156. if innerPos.Y+innerSize.Height > r.popUp.Canvas.Size().Height {
  157. innerPos.Y = r.popUp.Canvas.Size().Height - innerSize.Height
  158. if innerPos.Y < 0 {
  159. innerPos.Y = 0 // TODO here we may need a scroller as it's longer than our canvas
  160. }
  161. }
  162. r.popUp.Content.Move(innerPos.Add(r.offset()))
  163. r.background.Resize(innerSize)
  164. r.background.Move(innerPos)
  165. r.LayoutShadow(innerSize, innerPos)
  166. }
  167. func (r *popUpRenderer) MinSize() fyne.Size {
  168. return r.popUp.Content.MinSize().Add(r.padding())
  169. }
  170. func (r *popUpRenderer) Refresh() {
  171. r.background.FillColor = theme.OverlayBackgroundColor()
  172. expectedContentSize := r.popUp.innerSize.Max(r.popUp.MinSize()).Subtract(r.padding())
  173. shouldRelayout := r.popUp.Content.Size() != expectedContentSize
  174. if r.background.Size() != r.popUp.innerSize || r.background.Position() != r.popUp.innerPos || shouldRelayout {
  175. r.Layout(r.popUp.Size())
  176. }
  177. if r.popUp.Canvas.Size() != r.popUp.BaseWidget.Size() {
  178. r.popUp.BaseWidget.Resize(r.popUp.Canvas.Size())
  179. }
  180. r.popUp.Content.Refresh()
  181. r.background.Refresh()
  182. r.ShadowingRenderer.RefreshShadow()
  183. }
  184. type modalPopUpRenderer struct {
  185. *widget.ShadowingRenderer
  186. popUpBaseRenderer
  187. underlay *canvas.Rectangle
  188. }
  189. func (r *modalPopUpRenderer) Layout(canvasSize fyne.Size) {
  190. r.underlay.Resize(canvasSize)
  191. padding := r.padding()
  192. innerSize := r.popUp.innerSize.Max(r.popUp.Content.MinSize().Add(padding))
  193. requestedSize := innerSize.Subtract(padding)
  194. size := r.popUp.Content.MinSize().Max(requestedSize)
  195. size = size.Min(canvasSize.Subtract(padding))
  196. pos := fyne.NewPos((canvasSize.Width-size.Width)/2, (canvasSize.Height-size.Height)/2)
  197. r.popUp.Content.Move(pos)
  198. r.popUp.Content.Resize(size)
  199. innerPos := pos.Subtract(r.offset())
  200. r.background.Move(innerPos)
  201. r.background.Resize(size.Add(padding))
  202. r.LayoutShadow(innerSize, innerPos)
  203. }
  204. func (r *modalPopUpRenderer) MinSize() fyne.Size {
  205. return r.popUp.Content.MinSize().Add(r.padding())
  206. }
  207. func (r *modalPopUpRenderer) Refresh() {
  208. r.underlay.FillColor = theme.ShadowColor()
  209. r.background.FillColor = theme.OverlayBackgroundColor()
  210. expectedContentSize := r.popUp.innerSize.Max(r.popUp.MinSize()).Subtract(r.padding())
  211. shouldLayout := r.popUp.Content.Size() != expectedContentSize
  212. if r.background.Size() != r.popUp.innerSize || shouldLayout {
  213. r.Layout(r.popUp.Size())
  214. }
  215. if r.popUp.Canvas.Size() != r.popUp.BaseWidget.Size() {
  216. r.popUp.BaseWidget.Resize(r.popUp.Canvas.Size())
  217. }
  218. r.popUp.Content.Refresh()
  219. r.background.Refresh()
  220. }