select.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. package widget
  2. import (
  3. "image/color"
  4. "fyne.io/fyne/v2"
  5. "fyne.io/fyne/v2/canvas"
  6. "fyne.io/fyne/v2/driver/desktop"
  7. "fyne.io/fyne/v2/theme"
  8. )
  9. const defaultPlaceHolder string = "(Select one)"
  10. // Select widget has a list of options, with the current one shown, and triggers an event func when clicked
  11. type Select struct {
  12. DisableableWidget
  13. // Alignment sets the text alignment of the select and its list of options.
  14. //
  15. // Since: 2.1
  16. Alignment fyne.TextAlign
  17. Selected string
  18. Options []string
  19. PlaceHolder string
  20. OnChanged func(string) `json:"-"`
  21. focused bool
  22. hovered bool
  23. popUp *PopUpMenu
  24. tapAnim *fyne.Animation
  25. }
  26. var _ fyne.Widget = (*Select)(nil)
  27. var _ desktop.Hoverable = (*Select)(nil)
  28. var _ fyne.Tappable = (*Select)(nil)
  29. var _ fyne.Focusable = (*Select)(nil)
  30. var _ fyne.Disableable = (*Select)(nil)
  31. // NewSelect creates a new select widget with the set list of options and changes handler
  32. func NewSelect(options []string, changed func(string)) *Select {
  33. s := &Select{
  34. OnChanged: changed,
  35. Options: options,
  36. PlaceHolder: defaultPlaceHolder,
  37. }
  38. s.ExtendBaseWidget(s)
  39. return s
  40. }
  41. // ClearSelected clears the current option of the select widget. After
  42. // clearing the current option, the Select widget's PlaceHolder will
  43. // be displayed.
  44. func (s *Select) ClearSelected() {
  45. s.updateSelected("")
  46. }
  47. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  48. func (s *Select) CreateRenderer() fyne.WidgetRenderer {
  49. s.ExtendBaseWidget(s)
  50. s.propertyLock.RLock()
  51. icon := NewIcon(theme.MenuDropDownIcon())
  52. if s.PlaceHolder == "" {
  53. s.PlaceHolder = defaultPlaceHolder
  54. }
  55. txtProv := NewRichTextWithText(s.Selected)
  56. txtProv.inset = fyne.NewSize(theme.Padding(), theme.Padding())
  57. txtProv.ExtendBaseWidget(txtProv)
  58. txtProv.Truncation = fyne.TextTruncateEllipsis
  59. if s.disabled {
  60. txtProv.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled
  61. }
  62. background := &canvas.Rectangle{}
  63. tapBG := canvas.NewRectangle(color.Transparent)
  64. s.tapAnim = newButtonTapAnimation(tapBG, s)
  65. s.tapAnim.Curve = fyne.AnimationEaseOut
  66. objects := []fyne.CanvasObject{background, tapBG, txtProv, icon}
  67. r := &selectRenderer{icon, txtProv, background, objects, s}
  68. background.FillColor = r.bgColor()
  69. background.CornerRadius = theme.InputRadiusSize()
  70. r.updateIcon()
  71. s.propertyLock.RUnlock() // updateLabel and some text handling isn't quite right, resolve in text refactor for 2.0
  72. r.updateLabel()
  73. return r
  74. }
  75. // FocusGained is called after this Select has gained focus.
  76. //
  77. // Implements: fyne.Focusable
  78. func (s *Select) FocusGained() {
  79. s.focused = true
  80. s.Refresh()
  81. }
  82. // FocusLost is called after this Select has lost focus.
  83. //
  84. // Implements: fyne.Focusable
  85. func (s *Select) FocusLost() {
  86. s.focused = false
  87. s.Refresh()
  88. }
  89. // Hide hides the select.
  90. //
  91. // Implements: fyne.Widget
  92. func (s *Select) Hide() {
  93. if s.popUp != nil {
  94. s.popUp.Hide()
  95. s.popUp = nil
  96. }
  97. s.BaseWidget.Hide()
  98. }
  99. // MinSize returns the size that this widget should not shrink below
  100. func (s *Select) MinSize() fyne.Size {
  101. s.ExtendBaseWidget(s)
  102. return s.BaseWidget.MinSize()
  103. }
  104. // MouseIn is called when a desktop pointer enters the widget
  105. func (s *Select) MouseIn(*desktop.MouseEvent) {
  106. s.hovered = true
  107. s.Refresh()
  108. }
  109. // MouseMoved is called when a desktop pointer hovers over the widget
  110. func (s *Select) MouseMoved(*desktop.MouseEvent) {
  111. }
  112. // MouseOut is called when a desktop pointer exits the widget
  113. func (s *Select) MouseOut() {
  114. s.hovered = false
  115. s.Refresh()
  116. }
  117. // Move changes the relative position of the select.
  118. //
  119. // Implements: fyne.Widget
  120. func (s *Select) Move(pos fyne.Position) {
  121. s.BaseWidget.Move(pos)
  122. if s.popUp != nil {
  123. s.popUp.Move(s.popUpPos())
  124. }
  125. }
  126. // Resize sets a new size for a widget.
  127. // Note this should not be used if the widget is being managed by a Layout within a Container.
  128. func (s *Select) Resize(size fyne.Size) {
  129. s.BaseWidget.Resize(size)
  130. if s.popUp != nil {
  131. s.popUp.Resize(fyne.NewSize(size.Width, s.popUp.MinSize().Height))
  132. }
  133. }
  134. // SelectedIndex returns the index value of the currently selected item in Options list.
  135. // It will return -1 if there is no selection.
  136. func (s *Select) SelectedIndex() int {
  137. for i, option := range s.Options {
  138. if s.Selected == option {
  139. return i
  140. }
  141. }
  142. return -1 // not selected/found
  143. }
  144. // SetOptions updates the list of options available and refreshes the widget
  145. //
  146. // Since: 2.4
  147. func (s *Select) SetOptions(options []string) {
  148. s.Options = options
  149. s.Refresh()
  150. }
  151. // SetSelected sets the current option of the select widget
  152. func (s *Select) SetSelected(text string) {
  153. for _, option := range s.Options {
  154. if text == option {
  155. s.updateSelected(text)
  156. }
  157. }
  158. }
  159. // SetSelectedIndex will set the Selected option from the value in Options list at index position.
  160. func (s *Select) SetSelectedIndex(index int) {
  161. if index < 0 || index >= len(s.Options) {
  162. return
  163. }
  164. s.updateSelected(s.Options[index])
  165. }
  166. // Tapped is called when a pointer tapped event is captured and triggers any tap handler
  167. func (s *Select) Tapped(*fyne.PointEvent) {
  168. if s.Disabled() {
  169. return
  170. }
  171. s.tapAnimation()
  172. s.Refresh()
  173. s.showPopUp()
  174. }
  175. // TypedKey is called if a key event happens while this Select is focused.
  176. //
  177. // Implements: fyne.Focusable
  178. func (s *Select) TypedKey(event *fyne.KeyEvent) {
  179. switch event.Name {
  180. case fyne.KeySpace, fyne.KeyUp, fyne.KeyDown:
  181. s.showPopUp()
  182. case fyne.KeyRight:
  183. i := s.SelectedIndex() + 1
  184. if i >= len(s.Options) {
  185. i = 0
  186. }
  187. s.SetSelectedIndex(i)
  188. case fyne.KeyLeft:
  189. i := s.SelectedIndex() - 1
  190. if i < 0 {
  191. i = len(s.Options) - 1
  192. }
  193. s.SetSelectedIndex(i)
  194. }
  195. }
  196. // TypedRune is called if a text event happens while this Select is focused.
  197. //
  198. // Implements: fyne.Focusable
  199. func (s *Select) TypedRune(_ rune) {
  200. // intentionally left blank
  201. }
  202. func (s *Select) popUpPos() fyne.Position {
  203. buttonPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(s.super())
  204. return buttonPos.Add(fyne.NewPos(0, s.Size().Height-theme.InputBorderSize()))
  205. }
  206. func (s *Select) showPopUp() {
  207. items := make([]*fyne.MenuItem, len(s.Options))
  208. for i := range s.Options {
  209. text := s.Options[i] // capture
  210. items[i] = fyne.NewMenuItem(text, func() {
  211. s.updateSelected(text)
  212. s.popUp = nil
  213. })
  214. }
  215. c := fyne.CurrentApp().Driver().CanvasForObject(s.super())
  216. s.popUp = NewPopUpMenu(fyne.NewMenu("", items...), c)
  217. s.popUp.alignment = s.Alignment
  218. s.popUp.ShowAtPosition(s.popUpPos())
  219. s.popUp.Resize(fyne.NewSize(s.Size().Width, s.popUp.MinSize().Height))
  220. s.popUp.OnDismiss = func() {
  221. s.popUp.Hide()
  222. s.popUp = nil
  223. }
  224. }
  225. func (s *Select) tapAnimation() {
  226. if s.tapAnim == nil {
  227. return
  228. }
  229. s.tapAnim.Stop()
  230. if fyne.CurrentApp().Settings().ShowAnimations() {
  231. s.tapAnim.Start()
  232. }
  233. }
  234. func (s *Select) updateSelected(text string) {
  235. s.Selected = text
  236. if s.OnChanged != nil {
  237. s.OnChanged(s.Selected)
  238. }
  239. s.Refresh()
  240. }
  241. type selectRenderer struct {
  242. icon *Icon
  243. label *RichText
  244. background *canvas.Rectangle
  245. objects []fyne.CanvasObject
  246. combo *Select
  247. }
  248. func (s *selectRenderer) Objects() []fyne.CanvasObject {
  249. return s.objects
  250. }
  251. func (s *selectRenderer) Destroy() {}
  252. // Layout the components of the button widget
  253. func (s *selectRenderer) Layout(size fyne.Size) {
  254. s.background.Resize(fyne.NewSize(size.Width, size.Height))
  255. s.label.inset = fyne.NewSize(theme.Padding(), theme.Padding())
  256. iconPos := fyne.NewPos(size.Width-theme.IconInlineSize()-theme.InnerPadding(), (size.Height-theme.IconInlineSize())/2)
  257. labelSize := fyne.NewSize(iconPos.X-theme.Padding(), s.label.MinSize().Height)
  258. s.label.Resize(labelSize)
  259. s.label.Move(fyne.NewPos(theme.Padding(), (size.Height-labelSize.Height)/2))
  260. s.icon.Resize(fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize()))
  261. s.icon.Move(iconPos)
  262. }
  263. // MinSize calculates the minimum size of a select button.
  264. // This is based on the selected text, the drop icon and a standard amount of padding added.
  265. func (s *selectRenderer) MinSize() fyne.Size {
  266. s.combo.propertyLock.RLock()
  267. defer s.combo.propertyLock.RUnlock()
  268. minPlaceholderWidth := fyne.MeasureText(s.combo.PlaceHolder, theme.TextSize(), fyne.TextStyle{}).Width
  269. min := s.label.MinSize()
  270. min.Width = minPlaceholderWidth
  271. min = min.Add(fyne.NewSize(theme.InnerPadding()*3, theme.InnerPadding()))
  272. return min.Add(fyne.NewSize(theme.IconInlineSize()+theme.InnerPadding(), 0))
  273. }
  274. func (s *selectRenderer) Refresh() {
  275. s.combo.propertyLock.RLock()
  276. s.updateLabel()
  277. s.updateIcon()
  278. s.background.FillColor = s.bgColor()
  279. s.background.CornerRadius = theme.InputRadiusSize()
  280. s.combo.propertyLock.RUnlock()
  281. s.Layout(s.combo.Size())
  282. if s.combo.popUp != nil {
  283. s.combo.popUp.alignment = s.combo.Alignment
  284. s.combo.popUp.Move(s.combo.popUpPos())
  285. s.combo.popUp.Resize(fyne.NewSize(s.combo.size.Width, s.combo.popUp.MinSize().Height))
  286. s.combo.popUp.Refresh()
  287. }
  288. s.background.Refresh()
  289. canvas.Refresh(s.combo.super())
  290. }
  291. func (s *selectRenderer) bgColor() color.Color {
  292. if s.combo.disabled {
  293. return theme.DisabledButtonColor()
  294. }
  295. if s.combo.focused {
  296. return theme.FocusColor()
  297. }
  298. if s.combo.hovered {
  299. return theme.HoverColor()
  300. }
  301. return theme.InputBackgroundColor()
  302. }
  303. func (s *selectRenderer) updateIcon() {
  304. if s.combo.disabled {
  305. s.icon.Resource = theme.NewDisabledResource(theme.MenuDropDownIcon())
  306. } else {
  307. s.icon.Resource = theme.MenuDropDownIcon()
  308. }
  309. s.icon.Refresh()
  310. }
  311. func (s *selectRenderer) updateLabel() {
  312. if s.combo.PlaceHolder == "" {
  313. s.combo.PlaceHolder = defaultPlaceHolder
  314. }
  315. s.label.Segments[0].(*TextSegment).Style.Alignment = s.combo.Alignment
  316. if s.combo.disabled {
  317. s.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled
  318. } else {
  319. s.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground
  320. }
  321. if s.combo.Selected == "" {
  322. s.label.Segments[0].(*TextSegment).Text = s.combo.PlaceHolder
  323. } else {
  324. s.label.Segments[0].(*TextSegment).Text = s.combo.Selected
  325. }
  326. s.label.Refresh()
  327. }