package widget import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/internal/widget" ) // RadioGroup widget has a list of text labels and checks check icons next to each. // Changing the selection (only one can be selected) will trigger the changed func. // // Since: 1.4 type RadioGroup struct { DisableableWidget Horizontal bool Required bool OnChanged func(string) `json:"-"` Options []string Selected string // this index is ONE-BASED so the default zero-value is unselected // use r.selectedIndex(), r.setSelectedIndex(int) to maniupulate this field // as if it were a zero-based index (with -1 == nothing selected) _selIdx int } var _ fyne.Widget = (*RadioGroup)(nil) // NewRadioGroup creates a new radio group widget with the set options and change handler // // Since: 1.4 func NewRadioGroup(options []string, changed func(string)) *RadioGroup { r := &RadioGroup{ Options: options, OnChanged: changed, } r.ExtendBaseWidget(r) return r } // Append adds a new option to the end of a RadioGroup widget. func (r *RadioGroup) Append(option string) { r.Options = append(r.Options, option) r.Refresh() } // CreateRenderer is a private method to Fyne which links this widget to its renderer func (r *RadioGroup) CreateRenderer() fyne.WidgetRenderer { r.ExtendBaseWidget(r) items := make([]fyne.CanvasObject, len(r.Options)) for i, option := range r.Options { idx := i items[idx] = newRadioItem(option, func(item *radioItem) { r.itemTapped(item, idx) }) } render := &radioGroupRenderer{widget.NewBaseRenderer(items), items, r} r.updateSelectedIndex() render.updateItems(false) return render } // MinSize returns the size that this widget should not shrink below func (r *RadioGroup) MinSize() fyne.Size { r.ExtendBaseWidget(r) return r.BaseWidget.MinSize() } // SetSelected sets the radio option, it can be used to set a default option. func (r *RadioGroup) SetSelected(option string) { if r.Selected == option { return } r.Selected = option // selectedIndex will be updated on refresh to the first matching item if r.OnChanged != nil { r.OnChanged(option) } r.Refresh() } func (r *RadioGroup) itemTapped(item *radioItem, idx int) { if r.Disabled() { return } if r.selectedIndex() == idx { if r.Required { return } r.Selected = "" r.setSelectedIndex(-1) item.SetSelected(false) } else { r.Selected = item.Label r.setSelectedIndex(idx) item.SetSelected(true) } if r.OnChanged != nil { r.OnChanged(r.Selected) } r.Refresh() } func (r *RadioGroup) Refresh() { r.updateSelectedIndex() r.BaseWidget.Refresh() } func (r *RadioGroup) selectedIndex() int { return r._selIdx - 1 } func (r *RadioGroup) setSelectedIndex(idx int) { r._selIdx = idx + 1 } // if selectedIndex does not match the public Selected property, // set it to the index of the first radio item whose label matches Selected func (r *RadioGroup) updateSelectedIndex() { sel := r.Selected sIdx := r.selectedIndex() if sIdx >= 0 && sIdx < len(r.Options) && r.Options[sIdx] == sel { return // selected index matches Selected } if sIdx == -1 && sel == "" { return // nothing selected } sIdx = -1 for i, opt := range r.Options { if sel == opt { sIdx = i break } } r.setSelectedIndex(sIdx) } type radioGroupRenderer struct { widget.BaseRenderer // slice of *radioItem, but using fyne.CanvasObject as the type // so we can directly set it to the BaseRenderer's objects slice items []fyne.CanvasObject radio *RadioGroup } // Layout the components of the radio widget func (r *radioGroupRenderer) Layout(_ fyne.Size) { count := 1 if len(r.items) > 0 { count = len(r.items) } var itemHeight, itemWidth float32 minSize := r.radio.MinSize() if r.radio.Horizontal { itemHeight = minSize.Height itemWidth = minSize.Width / float32(count) } else { itemHeight = minSize.Height / float32(count) itemWidth = minSize.Width } itemSize := fyne.NewSize(itemWidth, itemHeight) x, y := float32(0), float32(0) for _, item := range r.items { item.Resize(itemSize) item.Move(fyne.NewPos(x, y)) if r.radio.Horizontal { x += itemWidth } else { y += itemHeight } } } // MinSize calculates the minimum size of a radio item. // This is based on the contained text, the radio icon and a standard amount of padding // between each item. func (r *radioGroupRenderer) MinSize() fyne.Size { width := float32(0) height := float32(0) for _, item := range r.items { itemMin := item.MinSize() width = fyne.Max(width, itemMin.Width) height = fyne.Max(height, itemMin.Height) } if r.radio.Horizontal { width = width * float32(len(r.items)) } else { height = height * float32(len(r.items)) } return fyne.NewSize(width, height) } func (r *radioGroupRenderer) Refresh() { r.updateItems(true) canvas.Refresh(r.radio.super()) } func (r *radioGroupRenderer) updateItems(refresh bool) { if len(r.items) < len(r.radio.Options) { for i := len(r.items); i < len(r.radio.Options); i++ { idx := i item := newRadioItem(r.radio.Options[idx], func(item *radioItem) { r.radio.itemTapped(item, idx) }) r.items = append(r.items, item) } r.Layout(r.radio.Size()) } else if len(r.items) > len(r.radio.Options) { total := len(r.radio.Options) r.items = r.items[:total] } r.SetObjects(r.items) for i, item := range r.items { item := item.(*radioItem) changed := false if l := r.radio.Options[i]; l != item.Label { item.Label = r.radio.Options[i] changed = true } if sel := i == r.radio.selectedIndex(); sel != item.Selected { item.Selected = sel changed = true } if d := r.radio.Disabled(); d != item.Disabled() { item.disabled = d changed = true } if refresh || changed { item.Refresh() } } }