check.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. package widget
  2. import (
  3. "fmt"
  4. "image/color"
  5. "fyne.io/fyne/v2"
  6. "fyne.io/fyne/v2/canvas"
  7. "fyne.io/fyne/v2/data/binding"
  8. "fyne.io/fyne/v2/driver/desktop"
  9. "fyne.io/fyne/v2/internal/widget"
  10. "fyne.io/fyne/v2/theme"
  11. )
  12. type checkRenderer struct {
  13. widget.BaseRenderer
  14. bg, icon *canvas.Image
  15. label *canvas.Text
  16. focusIndicator *canvas.Circle
  17. check *Check
  18. }
  19. // MinSize calculates the minimum size of a check.
  20. // This is based on the contained text, the check icon and a standard amount of padding added.
  21. func (c *checkRenderer) MinSize() fyne.Size {
  22. pad4 := theme.InnerPadding() * 2
  23. min := c.label.MinSize().Add(fyne.NewSize(theme.IconInlineSize()+pad4, pad4))
  24. if c.check.Text != "" {
  25. min.Add(fyne.NewSize(theme.Padding(), 0))
  26. }
  27. return min
  28. }
  29. // Layout the components of the check widget
  30. func (c *checkRenderer) Layout(size fyne.Size) {
  31. focusIndicatorSize := fyne.NewSquareSize(theme.IconInlineSize() + theme.InnerPadding())
  32. c.focusIndicator.Resize(focusIndicatorSize)
  33. c.focusIndicator.Move(fyne.NewPos(theme.InputBorderSize(), (size.Height-focusIndicatorSize.Height)/2))
  34. xOff := focusIndicatorSize.Width + theme.InputBorderSize()*2
  35. labelSize := size.SubtractWidthHeight(xOff, 0)
  36. c.label.Resize(labelSize)
  37. c.label.Move(fyne.NewPos(xOff, 0))
  38. iconPos := fyne.NewPos(theme.InnerPadding()/2+theme.InputBorderSize(), (size.Height-theme.IconInlineSize())/2)
  39. iconSize := fyne.NewSquareSize(theme.IconInlineSize())
  40. c.bg.Move(iconPos)
  41. c.bg.Resize(iconSize)
  42. c.icon.Resize(iconSize)
  43. c.icon.Move(iconPos)
  44. }
  45. // applyTheme updates this Check to the current theme
  46. func (c *checkRenderer) applyTheme() {
  47. c.label.Color = theme.ForegroundColor()
  48. c.label.TextSize = theme.TextSize()
  49. if c.check.disabled {
  50. c.label.Color = theme.DisabledColor()
  51. }
  52. }
  53. func (c *checkRenderer) Refresh() {
  54. c.check.propertyLock.RLock()
  55. c.applyTheme()
  56. c.updateLabel()
  57. c.updateResource()
  58. c.updateFocusIndicator()
  59. c.check.propertyLock.RUnlock()
  60. canvas.Refresh(c.check.super())
  61. }
  62. func (c *checkRenderer) updateLabel() {
  63. c.label.Text = c.check.Text
  64. }
  65. func (c *checkRenderer) updateResource() {
  66. res := theme.NewThemedResource(theme.CheckButtonIcon())
  67. res.ColorName = theme.ColorNameInputBorder
  68. // TODO move to `theme.CheckButtonFillIcon()` when we add it in 2.4
  69. bgRes := theme.NewThemedResource(fyne.CurrentApp().Settings().Theme().Icon("iconNameCheckButtonFill"))
  70. bgRes.ColorName = theme.ColorNameInputBackground
  71. if c.check.Checked {
  72. res = theme.NewThemedResource(theme.CheckButtonCheckedIcon())
  73. res.ColorName = theme.ColorNamePrimary
  74. bgRes.ColorName = theme.ColorNameBackground
  75. }
  76. if c.check.disabled {
  77. if c.check.Checked {
  78. res = theme.NewThemedResource(theme.CheckButtonCheckedIcon())
  79. }
  80. res.ColorName = theme.ColorNameDisabled
  81. bgRes.ColorName = theme.ColorNameBackground
  82. }
  83. c.icon.Resource = res
  84. c.bg.Resource = bgRes
  85. }
  86. func (c *checkRenderer) updateFocusIndicator() {
  87. if c.check.disabled {
  88. c.focusIndicator.FillColor = color.Transparent
  89. } else if c.check.focused {
  90. c.focusIndicator.FillColor = theme.FocusColor()
  91. } else if c.check.hovered {
  92. c.focusIndicator.FillColor = theme.HoverColor()
  93. } else {
  94. c.focusIndicator.FillColor = color.Transparent
  95. }
  96. }
  97. // Check widget has a text label and a checked (or unchecked) icon and triggers an event func when toggled
  98. type Check struct {
  99. DisableableWidget
  100. Text string
  101. Checked bool
  102. OnChanged func(bool) `json:"-"`
  103. focused bool
  104. hovered bool
  105. binder basicBinder
  106. }
  107. // Bind connects the specified data source to this Check.
  108. // The current value will be displayed and any changes in the data will cause the widget to update.
  109. // User interactions with this Check will set the value into the data source.
  110. //
  111. // Since: 2.0
  112. func (c *Check) Bind(data binding.Bool) {
  113. c.binder.SetCallback(c.updateFromData)
  114. c.binder.Bind(data)
  115. c.OnChanged = func(_ bool) {
  116. c.binder.CallWithData(c.writeData)
  117. }
  118. }
  119. // SetChecked sets the the checked state and refreshes widget
  120. func (c *Check) SetChecked(checked bool) {
  121. if checked == c.Checked {
  122. return
  123. }
  124. c.Checked = checked
  125. if c.OnChanged != nil {
  126. c.OnChanged(c.Checked)
  127. }
  128. c.Refresh()
  129. }
  130. // Hide this widget, if it was previously visible
  131. func (c *Check) Hide() {
  132. if c.focused {
  133. c.FocusLost()
  134. impl := c.super()
  135. if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil {
  136. c.Focus(nil)
  137. }
  138. }
  139. c.BaseWidget.Hide()
  140. }
  141. // MouseIn is called when a desktop pointer enters the widget
  142. func (c *Check) MouseIn(*desktop.MouseEvent) {
  143. if c.Disabled() {
  144. return
  145. }
  146. c.hovered = true
  147. c.Refresh()
  148. }
  149. // MouseOut is called when a desktop pointer exits the widget
  150. func (c *Check) MouseOut() {
  151. c.hovered = false
  152. c.Refresh()
  153. }
  154. // MouseMoved is called when a desktop pointer hovers over the widget
  155. func (c *Check) MouseMoved(*desktop.MouseEvent) {
  156. }
  157. // Tapped is called when a pointer tapped event is captured and triggers any change handler
  158. func (c *Check) Tapped(*fyne.PointEvent) {
  159. if !c.focused && !fyne.CurrentDevice().IsMobile() {
  160. impl := c.super()
  161. if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil {
  162. c.Focus(impl.(fyne.Focusable))
  163. }
  164. }
  165. if !c.Disabled() {
  166. c.SetChecked(!c.Checked)
  167. }
  168. }
  169. // MinSize returns the size that this widget should not shrink below
  170. func (c *Check) MinSize() fyne.Size {
  171. c.ExtendBaseWidget(c)
  172. return c.BaseWidget.MinSize()
  173. }
  174. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  175. func (c *Check) CreateRenderer() fyne.WidgetRenderer {
  176. c.ExtendBaseWidget(c)
  177. c.propertyLock.RLock()
  178. defer c.propertyLock.RUnlock()
  179. // TODO move to `theme.CheckButtonFillIcon()` when we add it in 2.4
  180. bg := canvas.NewImageFromResource(fyne.CurrentApp().Settings().Theme().Icon("iconNameCheckButtonFill"))
  181. icon := canvas.NewImageFromResource(theme.CheckButtonIcon())
  182. text := canvas.NewText(c.Text, theme.ForegroundColor())
  183. text.Alignment = fyne.TextAlignLeading
  184. focusIndicator := canvas.NewCircle(theme.BackgroundColor())
  185. r := &checkRenderer{
  186. widget.NewBaseRenderer([]fyne.CanvasObject{focusIndicator, bg, icon, text}),
  187. bg,
  188. icon,
  189. text,
  190. focusIndicator,
  191. c,
  192. }
  193. r.applyTheme()
  194. r.updateLabel()
  195. r.updateResource()
  196. r.updateFocusIndicator()
  197. return r
  198. }
  199. // NewCheck creates a new check widget with the set label and change handler
  200. func NewCheck(label string, changed func(bool)) *Check {
  201. c := &Check{
  202. Text: label,
  203. OnChanged: changed,
  204. }
  205. c.ExtendBaseWidget(c)
  206. return c
  207. }
  208. // NewCheckWithData returns a check widget connected with the specified data source.
  209. //
  210. // Since: 2.0
  211. func NewCheckWithData(label string, data binding.Bool) *Check {
  212. check := NewCheck(label, nil)
  213. check.Bind(data)
  214. return check
  215. }
  216. // FocusGained is called when the Check has been given focus.
  217. func (c *Check) FocusGained() {
  218. if c.Disabled() {
  219. return
  220. }
  221. c.focused = true
  222. c.Refresh()
  223. }
  224. // FocusLost is called when the Check has had focus removed.
  225. func (c *Check) FocusLost() {
  226. c.focused = false
  227. c.Refresh()
  228. }
  229. // TypedRune receives text input events when the Check is focused.
  230. func (c *Check) TypedRune(r rune) {
  231. if c.Disabled() {
  232. return
  233. }
  234. if r == ' ' {
  235. c.SetChecked(!c.Checked)
  236. }
  237. }
  238. // TypedKey receives key input events when the Check is focused.
  239. func (c *Check) TypedKey(key *fyne.KeyEvent) {}
  240. // SetText sets the text of the Check
  241. //
  242. // Since: 2.4
  243. func (c *Check) SetText(text string) {
  244. c.Text = text
  245. c.Refresh()
  246. }
  247. // Unbind disconnects any configured data source from this Check.
  248. // The current value will remain at the last value of the data source.
  249. //
  250. // Since: 2.0
  251. func (c *Check) Unbind() {
  252. c.OnChanged = nil
  253. c.binder.Unbind()
  254. }
  255. func (c *Check) updateFromData(data binding.DataItem) {
  256. if data == nil {
  257. return
  258. }
  259. boolSource, ok := data.(binding.Bool)
  260. if !ok {
  261. return
  262. }
  263. val, err := boolSource.Get()
  264. if err != nil {
  265. fyne.LogError("Error getting current data value", err)
  266. return
  267. }
  268. c.SetChecked(val) // if val != c.Checked, this will call updateFromData again, but only once
  269. }
  270. func (c *Check) writeData(data binding.DataItem) {
  271. if data == nil {
  272. return
  273. }
  274. boolTarget, ok := data.(binding.Bool)
  275. if !ok {
  276. return
  277. }
  278. currentValue, err := boolTarget.Get()
  279. if err != nil {
  280. return
  281. }
  282. if currentValue != c.Checked {
  283. err := boolTarget.Set(c.Checked)
  284. if err != nil {
  285. fyne.LogError(fmt.Sprintf("Failed to set binding value to %t", c.Checked), err)
  286. }
  287. }
  288. }