fyne icon indicating copy to clipboard operation
fyne copied to clipboard

Add widget.Switch for toggle switches

Open lostdusty opened this issue 2 years ago • 24 comments

Checklist

  • [X] I have searched the issue tracker for open issues that relate to the same feature, before opening a new one.
  • [X] This issue only relates to a single feature. I will open new issues for any other features.

Is your feature request related to a problem?

I think we should have an alternative to regular checkboxes, and toggle switches looks perfect for that.

Is it possible to construct a solution with the existing API?

I think they can be done using sliders, but I'm not sure about it.

Describe the solution you'd like to see.

I think they would match with Fyne's style, and I'd like the creation of it, like on the image below:

unnamed (1) Source: https://m2.material.io/components/switches

I also think the color could be managed by the theme primary color.

lostdusty avatar Nov 03 '23 01:11 lostdusty

The problem with this switch is that, even if you use primary for "on", the "off" state is not visible without comparison to on.

The question that I think leads to people adding :-1: is "why not use the Check widget that we already have?

I think we should have an alternative to regular checkboxes

Can you say /why/?

It is possible this could be added in fyne-x more easily as that does not have to have the "right way" to do any one thing ;).

andydotxyz avatar Nov 20 '23 17:11 andydotxyz

Not the OP, but my thoughts on "why toggle switches?"

  1. Toggle switches are a frequently used UI pattern for boolean options in modern apps.
  2. Users may be more familiar with seeing toggles than checkboxes. (for example, the "settings" UI of nearly every mobile OS).
  3. A form with a lot of color-highlighted toggles is slightly quicker to visually process than an equivalent field of checkboxes. (not required to take my word for it on this one, but try for yourself.)
  4. There are cases where a checkbox vs toggle carry slightly different connotations, and one may be more appropriate than the other. (Here's a discussion on that: https://blog.uxtweak.com/checkbox-vs-toggle-switch/)

Example from an iOS settings page: image

fieldse avatar Dec 05 '23 04:12 fieldse

I am interested in this and trying to implement this. But I don't know if the team of this project would like to accept this feature.

Dorbmon avatar Dec 18 '23 04:12 Dorbmon

There is always the fyne-x repository for community contributions

andydotxyz avatar Dec 18 '23 09:12 andydotxyz

I'd like to add to the accessibility reasons why this UI element should be baked in to core.

Here are two major usability reasons in mobile workflows that I know are important:

  • Visual representation is much more noticeable due to the size and coloring. Older people, like myself, and more visually impaired individuals can see this much better than a check toggle in all cases (disabled/enabled/untoggable state).
  • The size of the select switch is easier for people with larger fingers (like myself) or coordination disabilities (such as shaky hands) to interact with. A checkbox normally is akin to those tiny (x) they put in ads where you can't help but fat finger off the mark and hit some other part of the interface leading to undesirable results.

Although a check box can be made bigger and you probably could extend it to a color when toggled, it would end up bigger height wise in every row than common elements to remain square. Hmm, maybe a forced checkbox to scale and stretch width is doable already with existing containers or does it still scale sides one to one no matter what? However, I don't think there is anyway to color a checkbox and keep it looking 'clean and nice' in the current implementation. I need to experiment. Still this points out have a toggle that is accessibility friendly is harder work for users of the library where is the framework could make it easier.

So in summary, a select toggle is more about accessibility friendliness in clean UI design that the checkbox can't easily accomplish, it isn't just a replacement of an existing functionality. But I could see the possibility if the checkbox was extended to scale well horizontally or vertically and have easily configured states it could meet those needs without having to have a completely unique widget added to Fyne.

In my opinion, accessibility related widgets and functionality features should not be regulated to a community repo and could be an added strength and value of choosing Fyne over other frameworks. Either a select switch was added or checkbox had better options for accessibility concerns outlined here would be a great addition to the core repo.

beeblebrox avatar Sep 07 '24 19:09 beeblebrox

I am interested in this and trying to implement this. But I don't know if the team of this project would like to accept this feature.

Did you ever work on this and have it in a repo somewhere? I think a PR never hurts done in good faith, even if it isn't accepted it can help the community for anyone else that wants to merge it in to their UI.

beeblebrox avatar Sep 07 '24 19:09 beeblebrox

offSwitch onSwitch example switch image files ?

shofster avatar Sep 21 '24 16:09 shofster

@beeblebrox on the size topic, a check tap area is not limited to the "x" it does in fact respond to the same whole-height as a toggle would or as any of the buttons.

Moreover if your UI puts text attached to the check then that text will be tappable as well. The interaction area is massive.

In which Fyne app/demo did you find it hard to interact with?

andydotxyz avatar Sep 21 '24 19:09 andydotxyz

offSwitch

onSwitch

example switch image files ?

That looks very similar to a Slider, I think that would be confusing.

andydotxyz avatar Sep 21 '24 19:09 andydotxyz

I am interested in this and trying to implement this. But I don't know if the team of this project would like to accept this feature.

Did you ever work on this and have it in a repo somewhere? I think a PR never hurts done in good faith, even if it isn't accepted it can help the community for anyone else that wants to merge it in to their UI.

Fyne-x is a great place to submit widgets if the alignment with core is at all in question. Community are welcome to contribute and use widgets from there with far looser rules of contribution.

andydotxyz avatar Sep 21 '24 19:09 andydotxyz

@beeblebrox on the size topic, a check tap area is not limited to the "x" it does in fact respond to the same whole-height as a toggle would or as any of the buttons.

Moreover if your UI puts text attached to the check then that text will be tappable as well. The interaction area is massive.

In which Fyne app/demo did you find it hard to interact with?

@beeblebrox on the size topic, a check tap area is not limited to the "x" it does in fact respond to the same whole-height as a toggle would or as any of the buttons.

Moreover if your UI puts text attached to the check then that text will be tappable as well. The interaction area is massive.

In which Fyne app/demo did you find it hard to interact with? Let me clarify something first, a toggle by it's nature means it is enabled or not, a checkbox by its nature just indicates if something is selected out of usually a set of choices. They both come down to bools, but for UI design I think the distinction I made is common practice?

I haven't deployed this to mobile, so I am unsure how hard it would be to click, but with using a form layout to align each config+toggable line it currently looks like this:

Observe endpoint: [text input with endpoint config uri] <icon, online indicator for endpoint> [x]
  relay endpoint: [....text intput for endpoint...    ] <icon, online indicator of relay    > [x]

That 'x' is the checkbox widget and where I'd rather have the toggle to indicate if the observer/relay is going to be enabled/ processing to/from that endpoint. The icon represents the endpoint state, not if the function is enabled or not.

I am being minimalistic on purpose. Adding the text "enable" to expand the checkbox accessibility area would not fit the design. A toggle would be more concise, use a bit more horizontal space naturally, and more intuitive to the purpose without adding another text label.

Funny enough, I think it would also be more intuitive to have the toggle/checkbox on the left side. However it seems like the form usability can only align a text label for the first form column and then a set of containers as the second column that is aligned in the whole form. There isn't a way to move a widget that will align well on the left side of the labels. If that ability was there and I can put the text of the checkbox on the right and perhaps use the "Observe endpoint" like text to extend the toggable area of the checkbox for now, but that doesn't seem possible.

beeblebrox avatar Sep 22 '24 00:09 beeblebrox

example of switch and slider example

shofster avatar Sep 22 '24 15:09 shofster

example of switch and slider example

I appreciate this. Here's some feedback to take lightly. Starting to look good. Why isn't the toggle on line also thick? It looks a little funny to shrink that line. It is hard to say from this markups, but the toggle itself still looks tiny in comparison with a slider. In my opinion a toggle needs to still be a bit larger, the box at least the height and width of the slider as show in comparison with the text, one reason for a toggle is so it stands out really well.

Of course, I hope the enable/disable colors are theamable; I think many users would want the off switch line red.

Thanks for prototyping up some ideas!

beeblebrox avatar Sep 22 '24 16:09 beeblebrox

This is just an extended tappable Icon with 2 svg's created with inkscape, that alternate when clicked. I don't think icons are very customizable. Have to go the whole widget. The svg's are easy to create / modify. (colors, line thickness, ...)

On Sun, Sep 22, 2024, 10:51 AM James @.***> wrote:

example of switch and slider [image: example] https://private-user-images.githubusercontent.com/65819943/369697688-bbf5d702-75d7-4755-bc83-39885258c66a.jpg?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MjcwMjM3MzAsIm5iZiI6MTcyNzAyMzQzMCwicGF0aCI6Ii82NTgxOTk0My8zNjk2OTc2ODgtYmJmNWQ3MDItNzVkNy00NzU1LWJjODMtMzk4ODUyNThjNjZhLmpwZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA5MjIlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwOTIyVDE2NDM1MFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTdkMGVlZjcxOTliYjZhYjQzYjk0YTIyYmUzN2E4ZWQ1Yjk5ZGY1ODA2NzA0ZDZhNTg3Zjg0NzM0ZjFjOGJiZTQmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.gGvfu677sZeHwRk6EpMVzQ8Yjd9pqCXlYrg28Yocpkc

I appreciate this. Here's some feedback to take lightly. Starting to look good. Why isn't the toggle on line also thick? It looks a little funny to shrink that line. It is hard to say from this markups, but the toggle itself still looks tiny in comparison with a slider. In my opinion a toggle needs to still be a bit larger, the box at least the height and width of the slider as show in comparison with the text, one reason for a toggle is so it stands out really well.

Of course, I hope the enable/disable colors are theamable; I think many users would want the off switch line red.

Thanks for prototyping up some ideas!

— Reply to this email directly, view it on GitHub https://github.com/fyne-io/fyne/issues/4359#issuecomment-2366869386, or unsubscribe https://github.com/notifications/unsubscribe-auth/APWFKJ7ICIMHYNEDTW6EEMDZX3YRZAVCNFSM6AAAAABN2JC5MCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGNRWHA3DSMZYGY . You are receiving this because you are subscribed to this thread.Message ID: @.***>

shofster avatar Sep 22 '24 18:09 shofster

Icons can be completely replaced with a custom theme.

andydotxyz avatar Sep 22 '24 21:09 andydotxyz

The SVG files can be modified by hand to suit. Made larger circles. Fill color is modifiable too. newgit

shofster avatar Sep 23 '24 14:09 shofster

The SVG files can be modified by hand to suit. Made larger circles. Fill color is modifiable too. newgit I like the way these look.

I don't think any other widget that isn't just an icon in the fyne library requires to modify the svg completely to customize and fit a theme? One of the strengths of Fyne themes (I think?) is that common component look and feel, like colors, are easily changeable and maintain all the other looks? It is much easier to specify a color in one spot for all widgets common primary/secondary coloring than having to create a custom svg for a specific widget.

@shofster I am not discrediting your work, but maybe we can get it in and experiment in fyne-x first. I just think the customization is a bit to overhanded for the actual fyne library. Another point, a really smooth native widget for toggle might even be slightly animated when toggled. I think your proposal here is that basically two svg's will just flip flop instantly which might feel/look funky on any larger sized toggles instead of a smoother widget experience.

Again, this all just my opinion, I am just a user of Fyne and it really is the maintainers opinion that matters more on this than mine, but I hope they consider the feedback.

beeblebrox avatar Sep 23 '24 22:09 beeblebrox

Funny enough, I think it would also be more intuitive to have the toggle/checkbox on the left side. However it seems like the form usability can only align a text label for the first form column and then a set of containers as the second column that is aligned in the whole form.

@beeblebrox the check is by default leading-aligned (when you place text in the Check). If you want it to be in a form, then the label comes first with form widget second (on desktop), or above the item on portrait mobile apps. You can therefore have either alignment, depending on how you want to arrange the output.

andydotxyz avatar Sep 24 '24 14:09 andydotxyz

@beeblebrox the check is by default leading-aligned (when you place text in the Check). If you want it to be in a form, then the label comes first with form widget second (on desktop), or above the item on portrait mobile apps. You can therefore have either alignment, depending on how you want to arrange the output.

@andydotxyz the label in a form is just a string with the current fyne API, so I can't align the checkbox with text as an aligned label in a form column.

I don't want to derail this issue any more with this specific design issue, I'll ping in discord on this front when I have time to explain the issue better.

beeblebrox avatar Sep 24 '24 23:09 beeblebrox

For anyone needing this, I just went ahead and implemented it myself using simple canvas shapes (no svgs)

image image

Toggle Code
package custom_widgets

import (
	"fmt"
	"image/color"
	"sync"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/canvas"
	"fyne.io/fyne/v2/data/binding"
	"fyne.io/fyne/v2/driver/desktop"
	"fyne.io/fyne/v2/theme"
	"fyne.io/fyne/v2/widget"
)

type Toggle struct {
	widget.DisableableWidget
	Toggled bool

	togglePLock sync.RWMutex

	OnChanged func(bool) `json:"="`

	focused bool
	hovered bool

	binder binder

	minSize fyne.Size // cached for hover/top pos calcs
}

func NewToggle(changed func(bool)) *Toggle {
	t := &Toggle{
		OnChanged: changed,
	}
	t.ExtendBaseWidget(t)
	return t
}

func NewToggleWithData(data binding.Bool) *Toggle {
	toggle := NewToggle(nil)
	toggle.Bind(data)

	return toggle
}

func (t *Toggle) Bind(data binding.Bool) {
	t.binder.SetCallback(t.updateFromData)
	t.binder.Bind(data)

	t.OnChanged = func(_ bool) {
		t.binder.CallWithData(t.writeData)
	}
}

func (t *Toggle) SetToggled(toggled bool) {
	t.togglePLock.Lock()
	if toggled == t.Toggled {
		t.togglePLock.Unlock()
		return
	}

	t.Toggled = toggled
	onChanged := t.OnChanged
	t.togglePLock.Unlock()

	if onChanged != nil {
		onChanged(toggled)
	}

	t.Refresh()
}

func (t *Toggle) Hide() {
	if t.focused {
		t.FocusLost()
		if c := fyne.CurrentApp().Driver().CanvasForObject(t); c != nil {
			c.Focus(nil)
		}
	}
	t.BaseWidget.Hide()
}

func (t *Toggle) MouseIn(me *desktop.MouseEvent) {
	t.MouseMoved(me)
}

func (t *Toggle) MouseOut() {
	if t.hovered {
		t.hovered = false
		t.Refresh()
	}
}

func (t *Toggle) MouseMoved(me *desktop.MouseEvent) {
	if t.Disabled() {
		return
	}
	oldhovered := t.hovered

	t.hovered = t.minSize.IsZero() ||
		(me.Position.X <= t.minSize.Width && me.Position.Y <= t.minSize.Height)

	if oldhovered != t.hovered {
		t.Refresh()
	}
}

func (t *Toggle) Tapped(pe *fyne.PointEvent) {
	if t.Disabled() {
		return
	}
	if !t.minSize.IsZero() &&
		(pe.Position.X > t.minSize.Width || pe.Position.Y > t.minSize.Height) {
		// tapped outside
		return
	}

	if !t.focused {
		if !fyne.CurrentDevice().IsMobile() {
			if c := fyne.CurrentApp().Driver().CanvasForObject(t); c != nil {
				c.Focus(t)
			}
		}
	}
	t.SetToggled(!t.Toggled)
}

func (t *Toggle) MinSize() fyne.Size {
	t.ExtendBaseWidget(t)
	t.minSize = t.BaseWidget.MinSize()
	return t.minSize
}

func (t *Toggle) CreateRenderer() fyne.WidgetRenderer {
	th := t.Theme()
	v := fyne.CurrentApp().Settings().ThemeVariant()

	t.ExtendBaseWidget(t)

	var bgColor fyne.ThemeColorName
	if t.Toggled {
		bgColor = theme.ColorNamePrimary
	} else {
		bgColor = theme.ColorNameInputBackground
	}
	bg := canvas.NewRectangle(th.Color(bgColor, v))
	bg.StrokeColor = th.Color(theme.ColorNameInputBorder, v)
	bg.CornerRadius = th.Size(theme.SizeNameInlineIcon) / 2
	bg.StrokeWidth = th.Size(theme.SizeNameInputBorder)

	indicator := canvas.NewCircle(th.Color(theme.ColorNameForegroundOnPrimary, v))
	indicator.StrokeColor = th.Color(theme.ColorNameInputBorder, v)
	indicator.StrokeWidth = th.Size(theme.SizeNameInputBorder)

	t.togglePLock.RLock()
	defer t.togglePLock.RUnlock()

	focusIndicator := canvas.NewCircle(th.Color(theme.ColorNameBackground, v))

	r := &toggleRenderer{
		bg:             bg,
		indicator:      indicator,
		focusIndicator: focusIndicator,
		toggle:         t,
	}

	r.applyTheme(th, v)
	r.updateToggle(th, v)
	r.updateFocusIndicator(th, v)
	return r
}

func (t *Toggle) FocusGained() {
	if t.Disabled() {
		return
	}

	t.focused = true

	t.Refresh()
}

func (t *Toggle) FocusLost() {
	t.focused = false
	t.Refresh()
}

func (t *Toggle) TypedRune(r rune) {
	if t.Disabled() {
		return
	}
	if r == ' ' {
		t.SetToggled(!t.Toggled)
	}
}

func (t *Toggle) TypedKey(key *fyne.KeyEvent) {}

func (t *Toggle) Unbind() {
	t.OnChanged = nil
	t.binder.Unbind()
}

func (t *Toggle) updateFromData(data binding.DataItem) {
	if data == nil {
		return
	}
	boolSource, ok := data.(binding.Bool)
	if !ok {
		return
	}
	val, err := boolSource.Get()
	if err != nil {
		fyne.LogError("Error getting current data value", err)
		return
	}
	t.SetToggled(val)
}

func (t *Toggle) writeData(data binding.DataItem) {
	if data == nil {
		return
	}
	boolTarget, ok := data.(binding.Bool)
	if !ok {
		return
	}
	currentValue, err := boolTarget.Get()
	if err != nil {
		return
	}
	if currentValue != t.Toggled {
		err := boolTarget.Set(t.Toggled)
		if err != nil {
			fyne.LogError(fmt.Sprintf("Failed to set binding value to %t", t.Toggled), err)
		}
	}
}

type toggleRenderer struct {
	bg             *canvas.Rectangle
	indicator      *canvas.Circle
	focusIndicator *canvas.Circle
	toggle         *Toggle

	indicatorOffPos      fyne.Position
	indicatorOnPos       fyne.Position
	focusIndicatorOffPos fyne.Position
	focusIndicatorOnPos  fyne.Position
}

func (r *toggleRenderer) Destroy() {}
func (r *toggleRenderer) Objects() []fyne.CanvasObject {
	return []fyne.CanvasObject{
		r.bg,
		r.focusIndicator,
		r.indicator,
	}
}

func (t *toggleRenderer) MinSize() fyne.Size {
	th := t.toggle.Theme()

	pad4 := th.Size(theme.SizeNameInnerPadding)
	iconInline := th.Size(theme.SizeNameInlineIcon)
	borderSize := th.Size(theme.SizeNameInputBorder)
	min := fyne.NewSize(
		(iconInline*2)+pad4*2+borderSize,
		iconInline+pad4+borderSize,
	)

	return min
}

func (t *toggleRenderer) Layout(size fyne.Size) {
	th := t.toggle.Theme()
	innerPadding := th.Size(theme.SizeNameInnerPadding)
	borderSize := th.Size(theme.SizeNameInputBorder)
	iconInlineSize := th.Size(theme.SizeNameInlineIcon)

	t.indicatorOffPos = fyne.NewPos(
		innerPadding/2+borderSize,
		(size.Height-iconInlineSize-borderSize-innerPadding/2)/2,
	)
	indicatorSize := fyne.NewSquareSize(iconInlineSize + innerPadding/2)

	focusIndicatorSize := fyne.NewSquareSize(iconInlineSize + innerPadding)
	t.focusIndicatorOffPos = fyne.NewPos(
		innerPadding/4+borderSize,
		(size.Height-focusIndicatorSize.Height)/2,
	)
	t.indicatorOnPos = t.indicatorOffPos.AddXY(iconInlineSize+innerPadding, 0)
	t.focusIndicatorOnPos = t.focusIndicatorOffPos.AddXY(iconInlineSize+innerPadding, 0)

	t.toggle.togglePLock.RLock()
	toggled := t.toggle.Toggled
	t.toggle.togglePLock.RUnlock()
	t.focusIndicator.Resize(focusIndicatorSize)
	if toggled {
		t.indicator.Move(t.indicatorOnPos)
		t.focusIndicator.Move(t.focusIndicatorOnPos)
	} else {
		t.indicator.Move(t.indicatorOffPos)
		t.focusIndicator.Move(t.focusIndicatorOffPos)
	}

	bgPos := fyne.NewPos(
		innerPadding/2,
		(size.Height-iconInlineSize)/2,
	)
	bgSize := fyne.NewSize(iconInlineSize*2+innerPadding, iconInlineSize)
	t.bg.Resize(bgSize)
	t.bg.Move(bgPos)
	t.indicator.Resize(indicatorSize)
}

func (t *toggleRenderer) applyTheme(th fyne.Theme, v fyne.ThemeVariant) {
	if t.toggle.Disabled() {
		t.indicator.FillColor = th.Color(theme.ColorNameDisabled, v)
	} else {
		t.indicator.FillColor = th.Color(theme.ColorNameForegroundOnPrimary, v)
	}

	t.indicator.StrokeColor = th.Color(theme.ColorNameInputBorder, v)
	t.indicator.StrokeWidth = th.Size(theme.SizeNameInputBorder)

	t.bg.CornerRadius = th.Size(theme.SizeNameInlineIcon) / 2
	t.bg.StrokeWidth = th.Size(theme.SizeNameInputBorder)
}

func (t *toggleRenderer) Refresh() {
	th := t.toggle.Theme()
	v := fyne.CurrentApp().Settings().ThemeVariant()

	t.toggle.togglePLock.RLock()
	t.applyTheme(th, v)
	t.updateFocusIndicator(th, v)
	t.updateToggle(th, v)
	t.toggle.togglePLock.RUnlock()
}

func (t *toggleRenderer) updateFocusIndicator(th fyne.Theme, v fyne.ThemeVariant) {
	if t.toggle.Disabled() {
		t.focusIndicator.FillColor = color.Transparent
	} else if t.toggle.focused {
		t.focusIndicator.FillColor = th.Color(theme.ColorNameFocus, v)
	} else if t.toggle.hovered {
		t.focusIndicator.FillColor = th.Color(theme.ColorNameHover, v)
	} else {
		t.focusIndicator.FillColor = color.Transparent
	}

	if t.toggle.Toggled {
		t.focusIndicator.Move(t.focusIndicatorOnPos)
	} else {
		t.focusIndicator.Move(t.focusIndicatorOffPos)
	}
}

func (t *toggleRenderer) updateToggle(th fyne.Theme, v fyne.ThemeVariant) {
	if t.toggle.Toggled {
		t.indicator.Move(t.indicatorOnPos)
		t.bg.FillColor = th.Color(theme.ColorNamePrimary, v)
	} else {
		t.indicator.Move(t.indicatorOffPos)
		t.bg.FillColor = th.Color(theme.ColorNameInputBackground, v)
	}
}

binder it just a copy of the internal fyne binder

package custom_widgets
import (
	"sync"
	"sync/atomic"

	"fyne.io/fyne/v2/data/binding"
)

type listerPair struct {
	data     binding.DataItem
	listener binding.DataListener
}

type binder struct {
	callback atomic.Pointer[func(binding.DataItem)]
	lock     sync.RWMutex
	pair     listerPair // guarded by lock
}

func (b *binder) Bind(data binding.DataItem) {
	listener := binding.NewDataListener(func() {
		f := b.callback.Load()
		if f == nil || *f == nil {
			return
		}
		(*f)(data)
	})
	data.AddListener(listener)
	pair := listerPair{
	  data: data,
	  listener: listener,
	}
	b.lock.Lock()
	b.unbindLocked()
	b.pair =pair
	b.lock.Unlock()
}

func (b *binder) CallWithData(f func(data binding.DataItem)) {
  b.lock.RLock()
  data := b.pair.data
  b.lock.RUnlock()
  f(data)
}

func (b *binder) SetCallback(f func(data binding.DataItem)) {
  b.callback.Store(&f)
}

func (b *binder) Unbind() {
  b.lock.Lock()
  b.unbindLocked()
  b.lock.Unlock()
}

func(b *binder) unbindLocked() {
  prev := b.pair
  b.pair = listerPair{nil, nil}
  if prev.listener == nil || prev.data == nil {
    return
  }
  prev.data.RemoveListener(prev.listener)
}

Ryex avatar Oct 01 '24 23:10 Ryex

Looks good!

lostdusty avatar Oct 02 '24 00:10 lostdusty

Why not open a PR on fyne-x? I'm sure the the folk who do want to use this would appreciate the shared component...

andydotxyz avatar Oct 02 '24 09:10 andydotxyz

A different SVG version.

import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/widget" )

type SwitchIcon struct { widget.Icon checked bool off *fyne.StaticResource on *fyne.StaticResource OnChanged func(bool) // leave nil for "status" like usage }

func NewSwitchIcon(changed func(bool)) *SwitchIcon { o := &SwitchIcon{ OnChanged: changed, off: resourceOffSwitchSvg, on: resourceOnSwitchSvg, } o.ExtendBaseWidget(o) o.SetResource(resourceOffSwitchSvg) return o }

// Tapped processes a single click func (o *SwitchIcon) Tapped(_ *fyne.PointEvent) { if o.OnChanged != nil { // only a status, not user control o.SetChecked(!o.checked) } }

// SetChecked sets the checked state, refreshes icon, calls back (maybe) func (o *SwitchIcon) SetChecked(checked bool) { o.checked = checked switch o.checked { case false: o.SetResource(o.off) case true: o.SetResource(o.on) } if o.OnChanged != nil { o.OnChanged(o.checked) } }

// GetChecked returns the checked state func (o *SwitchIcon) GetChecked() bool { return o.checked }

func GetOffIcon() *fyne.StaticResource { return resourceOffSwitchSvg } func GetOnIcon() *fyne.StaticResource { return resourceOnSwitchSvg }

var resourceOffSwitchSvg = &fyne.StaticResource{ StaticName: "OffSwitch.svg", StaticContent: []byte( "<svg\n width="200"\n height="100"\n viewBox="0 0 79.374998 52.916666"\n xmlns="http://www.w3.org/2000/svg">\n \n <path\n style="fill:#f9f9f9;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-miterlimit:77.8;stroke-dasharray:none;stroke-opacity:0.990783;paint-order:markers stroke fill"\n id="rect1"\n d="M 15.431886,2.5698392 H 65.701602 A 13.229167,13.229167 45 0 1 78.930769,15.799006 V 37.433377 A 13.229167,13.229167 135 0 1 65.701602,50.662544 H 15.431886 A 13.229167,13.229167 45 0 1 2.2027192,37.433377 V 15.799006 A 13.229167,13.229167 135 0 1 15.431886,2.5698392 Z"\n transform="matrix(0.97216082,0,0,1.0013863,0.02798535,-0.03689924)"/>\n <ellipse\n style="fill:#e6e6e6;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-miterlimit:77.8;stroke-dasharray:none;stroke-opacity:0.990783;paint-order:markers stroke fill"\n id="path1"\n cx="28"\n cy="26"\n rx="24"\n ry="22"/>\n <rect\n style="fill:#e6e6e6;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-miterlimit:77.8;stroke-dasharray:none;stroke-opacity:0.990783;paint-order:markers stroke fill"\n id="rect2"\n width="19"\n height="10"\n x="52"\n y="22"/>\n \n\n"), } var resourceOnSwitchSvg = &fyne.StaticResource{ StaticName: "OnSwitch.svg", StaticContent: []byte( "<svg\n width="200"\n height="100"\n viewBox="0 0 79.374998 52.916666"\n xmlns="http://www.w3.org/2000/svg">\n \n <path\n style="fill:#f9f9f9;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-miterlimit:77.8;stroke-dasharray:none;stroke-opacity:0.990783;paint-order:markers stroke fill"\n id="rect1"\n d="M 9.4059795,1.8355994 H 68.790562 a 7.9375,7.9375 45 0 1 7.9375,7.9375 V 42.357923 a 7.9375,7.9375 135 0 1 -7.9375,7.9375 H 9.4059795 a 7.9375,7.9375 45 0 1 -7.9375,-7.9375 V 9.7730994 a 7.9375,7.9375 135 0 1 7.9375,-7.9375 z"\n transform="matrix(0.97632835,0,0,1.001116,1.8433205,-0.02908883)"/>\n <ellipse\n style="fill:#00ff00;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-miterlimit:77.8;stroke-opacity:0.990783;paint-order:markers stroke fill"\n id="path1"\n cx="50"\n cy="26"\n rx="24"\n ry="22"/>\n <rect\n style="fill:#00ff00;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-miterlimit:77.8;stroke-dasharray:none;stroke-opacity:0.990783;paint-order:markers stroke fill"\n id="rect2"\n width="18"\n height="10"\n x="8"\n y="21"/>\n \n\n"), }

On Wed, Oct 2, 2024 at 3:07 AM Andy Williams @.***> wrote:

Why not open a PR on fyne-x? I'm sure the the folk who do want to use this would appreciate the shared component...

— Reply to this email directly, view it on GitHub https://github.com/fyne-io/fyne/issues/4359#issuecomment-2387984621, or unsubscribe https://github.com/notifications/unsubscribe-auth/APWFKJ4SZIJGGI5HHFMJI53ZZOZUBAVCNFSM6AAAAABN2JC5MCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDGOBXHE4DINRSGE . You are receiving this because you were mentioned.Message ID: @.***>

shofster avatar Oct 02 '24 16:10 shofster

IMO I prefer the version using canvas shapes, as it could follow the Fyne's primary theme color, and could be animated later down the road. The svg version is a good start, but I don't think it's the right path for switches.

lostdusty avatar Oct 02 '24 19:10 lostdusty