popup.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  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. // ShowAtRelativePosition shows this pop-up at the given position relative to stated object.
  63. //
  64. // Since 2.4
  65. func (p *PopUp) ShowAtRelativePosition(rel fyne.Position, to fyne.CanvasObject) {
  66. withRelativePosition(rel, to, p.ShowAtPosition)
  67. }
  68. // Tapped is called when the user taps the popUp background - if not modal then dismiss this widget
  69. func (p *PopUp) Tapped(_ *fyne.PointEvent) {
  70. if !p.modal {
  71. p.Hide()
  72. }
  73. }
  74. // TappedSecondary is called when the user right/alt taps the background - if not modal then dismiss this widget
  75. func (p *PopUp) TappedSecondary(_ *fyne.PointEvent) {
  76. if !p.modal {
  77. p.Hide()
  78. }
  79. }
  80. // MinSize returns the size that this widget should not shrink below
  81. func (p *PopUp) MinSize() fyne.Size {
  82. p.ExtendBaseWidget(p)
  83. return p.BaseWidget.MinSize()
  84. }
  85. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  86. func (p *PopUp) CreateRenderer() fyne.WidgetRenderer {
  87. p.ExtendBaseWidget(p)
  88. background := canvas.NewRectangle(theme.OverlayBackgroundColor())
  89. if p.modal {
  90. underlay := canvas.NewRectangle(theme.ShadowColor())
  91. objects := []fyne.CanvasObject{underlay, background, p.Content}
  92. return &modalPopUpRenderer{
  93. widget.NewShadowingRenderer(objects, widget.DialogLevel),
  94. popUpBaseRenderer{popUp: p, background: background},
  95. underlay,
  96. }
  97. }
  98. objects := []fyne.CanvasObject{background, p.Content}
  99. return &popUpRenderer{
  100. widget.NewShadowingRenderer(objects, widget.PopUpLevel),
  101. popUpBaseRenderer{popUp: p, background: background},
  102. }
  103. }
  104. // ShowPopUpAtPosition creates a new popUp for the specified content at the specified absolute position.
  105. // It will then display the popup on the passed canvas.
  106. func ShowPopUpAtPosition(content fyne.CanvasObject, canvas fyne.Canvas, pos fyne.Position) {
  107. newPopUp(content, canvas).ShowAtPosition(pos)
  108. }
  109. // ShowPopUpAtRelativePosition shows a new popUp for the specified content at the given position relative to stated object.
  110. // It will then display the popup on the passed canvas.
  111. //
  112. // Since 2.4
  113. func ShowPopUpAtRelativePosition(content fyne.CanvasObject, canvas fyne.Canvas, rel fyne.Position, to fyne.CanvasObject) {
  114. withRelativePosition(rel, to, func(pos fyne.Position) {
  115. ShowPopUpAtPosition(content, canvas, pos)
  116. })
  117. }
  118. func newPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp {
  119. ret := &PopUp{Content: content, Canvas: canvas, modal: false}
  120. ret.ExtendBaseWidget(ret)
  121. return ret
  122. }
  123. // NewPopUp creates a new popUp for the specified content and displays it on the passed canvas.
  124. func NewPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp {
  125. return newPopUp(content, canvas)
  126. }
  127. // ShowPopUp creates a new popUp for the specified content and displays it on the passed canvas.
  128. func ShowPopUp(content fyne.CanvasObject, canvas fyne.Canvas) {
  129. newPopUp(content, canvas).Show()
  130. }
  131. func newModalPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp {
  132. p := &PopUp{Content: content, Canvas: canvas, modal: true}
  133. p.ExtendBaseWidget(p)
  134. return p
  135. }
  136. // NewModalPopUp creates a new popUp for the specified content and displays it on the passed canvas.
  137. // A modal PopUp blocks interactions with underlying elements, covered with a semi-transparent overlay.
  138. func NewModalPopUp(content fyne.CanvasObject, canvas fyne.Canvas) *PopUp {
  139. return newModalPopUp(content, canvas)
  140. }
  141. // ShowModalPopUp creates a new popUp for the specified content and displays it on the passed canvas.
  142. // A modal PopUp blocks interactions with underlying elements, covered with a semi-transparent overlay.
  143. func ShowModalPopUp(content fyne.CanvasObject, canvas fyne.Canvas) {
  144. p := newModalPopUp(content, canvas)
  145. p.Show()
  146. }
  147. type popUpBaseRenderer struct {
  148. popUp *PopUp
  149. background *canvas.Rectangle
  150. }
  151. func (r *popUpBaseRenderer) padding() fyne.Size {
  152. return fyne.NewSize(theme.InnerPadding(), theme.InnerPadding())
  153. }
  154. func (r *popUpBaseRenderer) offset() fyne.Position {
  155. return fyne.NewPos(theme.InnerPadding()/2, theme.InnerPadding()/2)
  156. }
  157. type popUpRenderer struct {
  158. *widget.ShadowingRenderer
  159. popUpBaseRenderer
  160. }
  161. func (r *popUpRenderer) Layout(_ fyne.Size) {
  162. innerSize := r.popUp.innerSize.Max(r.popUp.MinSize())
  163. r.popUp.Content.Resize(innerSize.Subtract(r.padding()))
  164. innerPos := r.popUp.innerPos
  165. if innerPos.X+innerSize.Width > r.popUp.Canvas.Size().Width {
  166. innerPos.X = r.popUp.Canvas.Size().Width - innerSize.Width
  167. if innerPos.X < 0 {
  168. innerPos.X = 0 // TODO here we may need a scroller as it's wider than our canvas
  169. }
  170. }
  171. if innerPos.Y+innerSize.Height > r.popUp.Canvas.Size().Height {
  172. innerPos.Y = r.popUp.Canvas.Size().Height - innerSize.Height
  173. if innerPos.Y < 0 {
  174. innerPos.Y = 0 // TODO here we may need a scroller as it's longer than our canvas
  175. }
  176. }
  177. r.popUp.Content.Move(innerPos.Add(r.offset()))
  178. r.background.Resize(innerSize)
  179. r.background.Move(innerPos)
  180. r.LayoutShadow(innerSize, innerPos)
  181. }
  182. func (r *popUpRenderer) MinSize() fyne.Size {
  183. return r.popUp.Content.MinSize().Add(r.padding())
  184. }
  185. func (r *popUpRenderer) Refresh() {
  186. r.background.FillColor = theme.OverlayBackgroundColor()
  187. expectedContentSize := r.popUp.innerSize.Max(r.popUp.MinSize()).Subtract(r.padding())
  188. shouldRelayout := r.popUp.Content.Size() != expectedContentSize
  189. if r.background.Size() != r.popUp.innerSize || r.background.Position() != r.popUp.innerPos || shouldRelayout {
  190. r.Layout(r.popUp.Size())
  191. }
  192. if r.popUp.Canvas.Size() != r.popUp.BaseWidget.Size() {
  193. r.popUp.BaseWidget.Resize(r.popUp.Canvas.Size())
  194. }
  195. r.popUp.Content.Refresh()
  196. r.background.Refresh()
  197. r.ShadowingRenderer.RefreshShadow()
  198. }
  199. type modalPopUpRenderer struct {
  200. *widget.ShadowingRenderer
  201. popUpBaseRenderer
  202. underlay *canvas.Rectangle
  203. }
  204. func (r *modalPopUpRenderer) Layout(canvasSize fyne.Size) {
  205. r.underlay.Resize(canvasSize)
  206. padding := r.padding()
  207. innerSize := r.popUp.innerSize.Max(r.popUp.Content.MinSize().Add(padding))
  208. requestedSize := innerSize.Subtract(padding)
  209. size := r.popUp.Content.MinSize().Max(requestedSize)
  210. size = size.Min(canvasSize.Subtract(padding))
  211. pos := fyne.NewPos((canvasSize.Width-size.Width)/2, (canvasSize.Height-size.Height)/2)
  212. r.popUp.Content.Move(pos)
  213. r.popUp.Content.Resize(size)
  214. innerPos := pos.Subtract(r.offset())
  215. r.background.Move(innerPos)
  216. r.background.Resize(size.Add(padding))
  217. r.LayoutShadow(innerSize, innerPos)
  218. }
  219. func (r *modalPopUpRenderer) MinSize() fyne.Size {
  220. return r.popUp.Content.MinSize().Add(r.padding())
  221. }
  222. func (r *modalPopUpRenderer) Refresh() {
  223. r.underlay.FillColor = theme.ShadowColor()
  224. r.background.FillColor = theme.OverlayBackgroundColor()
  225. expectedContentSize := r.popUp.innerSize.Max(r.popUp.MinSize()).Subtract(r.padding())
  226. shouldLayout := r.popUp.Content.Size() != expectedContentSize
  227. if r.background.Size() != r.popUp.innerSize || shouldLayout {
  228. r.Layout(r.popUp.Size())
  229. }
  230. if r.popUp.Canvas.Size() != r.popUp.BaseWidget.Size() {
  231. r.popUp.BaseWidget.Resize(r.popUp.Canvas.Size())
  232. }
  233. r.popUp.Content.Refresh()
  234. r.background.Refresh()
  235. }
  236. func withRelativePosition(rel fyne.Position, to fyne.CanvasObject, f func(position fyne.Position)) {
  237. d := fyne.CurrentApp().Driver()
  238. c := d.CanvasForObject(to)
  239. if c == nil {
  240. fyne.LogError("Could not locate parent object to display relative to", nil)
  241. f(rel)
  242. return
  243. }
  244. pos := d.AbsolutePositionForObject(to).Add(rel)
  245. f(pos)
  246. }