package screen import ( "errors" "fmt" "time" "bitbucket.org/hevanto/ui/uiwidget" "fyne.io/fyne/v2" "fyne.io/fyne/v2/data/binding" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" xwidget "fyne.io/x/fyne/widget" ) // The constants for the Widgets // // Available yaml options for all widgets: // - id: The ID for exporting the widget to code // - decorators: The decorators to wrap the widget into // - hidden: Wether the widget is hidden or not type Widget string const ( // Button Widget // // Available yaml options: // - text: The text for the button // - localized: If the text represents a localization key // - icon: The icon for the button // - disabled: Wether the button is disabled or not // - options: Additional options for the button // - alignment: The alignment of the button // - iconPlacement: The icon placement of the button // - importance: The importance of the button // - onclicked: The function to call when the button is clicked Button Widget = "Button" // Calendar Widget // // Available yaml options: // - time: The current time to use for the calendar // - timeFormat: The format to use to interpret the time field (default time.DateOnly) // - onDateSelected: The function to call when a date is selected Calendar Widget = "Calendar" // Checkbox Widget // // Available yaml options: // - text: The text for the checkbox // - localized: If the text represents a localization key // - binding: The databinding for the checkbox // - checked: The checked state of the checkbox // - disabled: Wether the button is disabled or not // - onChanged: The function to call when the checkbox is changed Check Widget = "Check" DateEntry Widget = "DateEntry" // Entry Widget // // Available yaml options: // - text: The text for the entry // - localized: If the text represents a localization key // - binding: The databinding for the entry // - placeholder: The placeholder for the entry // - placeholderLocalized: If the placeholder represents a localization key // - multiLine: If the entry is multiline // - disabled: Wether the button is disabled or not // - validator: The validator for the entry // - onValidationChanged: The function to call when the validation changes Entry Widget = "Entry" // H1 Widget // // Available yaml options: // - text: The text for the H1 // - localized: If the text represents a localization key // - binding: The databinding for the H1 // - options: The options for the H1 // - wrapping: The wrapping option for the H1 // - truncation: The truncation option for the H1 H1 Widget = "H1" // H2 Widget // // Available yaml options: // - text: The text for the H2 // - localized: If the text represents a localization key // - binding: The databinding for the H2 // - options: The options for the H2 // - wrapping: The wrapping option for the H2 // - truncation: The truncation option for the H2 H2 Widget = "H2" // H3 Widget // // Available yaml options: // - text: The text for the H3 // - localized: If the text represents a localization key // - binding: The databinding for the H3 // - options: The options for the H3 // - wrapping: The wrapping option for the H3 // - truncation: The truncation option for the H3 H3 Widget = "H3" // H4 Widget // // Available yaml options: // - text: The text for the H4 // - localized: If the text represents a localization key // - binding: The databinding for the H4 // - options: The options for the H4 // - wrapping: The wrapping option for the H4 // - truncation: The truncation option for the H4 H4 Widget = "H4" // H5 Widget // // Available yaml options: // - text: The text for the H5 // - localized: If the text represents a localization key // - binding: The databinding for the H5 // - options: The options for the H5 // - wrapping: The wrapping option for the H5 // - truncation: The truncation option for the H5 H5 Widget = "H5" // H6 Widget // // Available yaml options: // - text: The text for the H6 // - localized: If the text represents a localization key // - binding: The databinding for the H6 // - options: The options for the H6 // - wrapping: The wrapping option for the H6 // - truncation: The truncation option for the H6 H6 Widget = "H6" // Icon Widget // // Available yaml options: // - icon: The icon for the icon Icon Widget = "Icon" // Label Widget // // Available yaml options: // - text: The text for the label // - localized: If the text represents a localization key // - binding: The databinding for the label // - options: The options for the label // - alignment: The alignment option for the label // - wrapping: The wrapping option for the label // - textStyle: The text style option for the label // - truncation: The truncation option for the label Label Widget = "Label" // List Widget // // Available yaml options: // - binding: The databinding for the list // - listLength: The length of the list // - itemTemplate: The item template for the list // - itemRenderer: The item renderer for the list // - onSelect: The on select function for the list // - onUnselect: The on unselect function for the list List Widget = "List" // Select Widget Select Widget = "Select" // SelectEntry Widget SelectEntry Widget = "SelectEntry" // Separator Widget Separator Widget = "Separator" // Spacer Widget Spacer Widget = "Spacer" // UpDownLabel Widget // // Available yaml options: // - binding: The databinding for the up down label // - onUpClicked: The on up clicked function for the up down label // - onDownClicked: The on down clicked function for the up down label UpDownLabel Widget = "UpDownLabel" ) // Build builds a fyne.CanvasObject for the given Widget and Element, // using the provided ScreenHandler to fetch functions, data bindings, etc. // // Arguments: // - e: The Element to build the Widget for. // - s: The ScreenHandler to use to fetch functions, data bindings, etc. // // Returns the CanvasObject for the widget, the CanvasObject for the decorator, // and an error if any. // If no decorators are specified, the widget and decorator will be the same. func (w Widget) Build( e *Element, s ScreenHandler, ) ( widget fyne.CanvasObject, decorator fyne.CanvasObject, err error, ) { e.localize(s) switch w { case Button: widget, err = w.buildButtonWidget(e, s) case Calendar: widget, err = w.buildCalendarWidget(e, s) case Check: widget, err = w.buildCheckWidget(e, s) case DateEntry: widget, err = w.buildDateEntryWidget(e, s) case Entry: widget, err = w.buildEntryWidget(e, s) case H1, H2, H3, H4, H5, H6: widget, err = w.buildHeaderLabelWidget(e, s) case Icon: widget, err = w.buildIconWidget(e, s) case Label: widget, err = w.buildLabelWidget(e, s) case List: widget, err = w.buildListWidget(e, s) case Select: widget, err = w.buildSelectWidget(e, s) case SelectEntry: widget, err = w.buildSelectEntryWidget(e, s) case Separator: widget, err = w.buildSeparatorWidget(e, s) case Spacer: widget, err = w.buildSpacerWidget(e, s) case UpDownLabel: widget, err = w.buildUpDownLabelWidget(e, s) default: err = errors.New("invalid widget") } if err != nil { return } decorator = widget if e.Decorators != nil { for _, dec := range e.Decorators { switch dec { case "Border": decorator = uiwidget.NewWidgetBorder(decorator) } } } if e.Hidden { decorator.Hide() } return } func (w Widget) buildButtonWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { var btn *widget.Button if e.Icon != "" { btn = widget.NewButtonWithIcon( e.Text, s.GetIcon(e.Icon), s.GetOnClickedHandler(e.OnClicked)) } else { btn = widget.NewButton(e.Text, s.GetOnClickedHandler(e.OnClicked)) } for opt, val := range e.Options { switch opt { case "alignment": btn.Alignment = getButtonAlignment(val) case "iconPlacement": btn.IconPlacement = getButtonIconPlacement(val) case "importance": btn.Importance = getImportance(val) } } if e.Disabled { btn.Disable() } c = btn return } func (w Widget) buildCalendarWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { t := time.Now() if e.Time != nil { timeFormat := time.DateOnly if e.TimeFormat != nil { timeFormat = parseTimeFormat(*e.TimeFormat) } if t, err = time.Parse(timeFormat, *e.Time); err != nil { err = fmt.Errorf("CalendarWidget: failed to parse time: %w", err) return } } c = xwidget.NewCalendar(t, s.GetOnDateSelectedHandler(e.OnDateSelected)) return } func (w Widget) buildCheckWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { var chk *widget.Check if e.Binding != "" { chk = widget.NewCheckWithData( e.Text, s.GetBinding(e.Binding).(binding.Bool)) } else { chk = widget.NewCheck( e.Text, s.GetOnCheckChangedHandler(e.OnChanged)) chk.SetChecked(e.Checked) } if e.Disabled { chk.Disable() } c = chk return } func (w Widget) buildDateEntryWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { ent := uiwidget.NewDateEntry() if e.OnDateSelected != "" { ent.OnDateChanged = s.GetOnDateSelectedHandler(e.OnDateSelected) } if e.Disabled { ent.Disable() } c = ent return } func (w Widget) buildEntryWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { var ent *widget.Entry if e.MultiLine { ent = widget.NewMultiLineEntry() } else { ent = widget.NewEntry() } if e.Binding != "" { ent.Bind(s.GetBinding(e.Binding).(binding.String)) } else { ent.Text = e.Text } ent.PlaceHolder = e.Placeholder if e.Validator != "" { ent.Validator = s.GetValidator(e.Validator) } if e.OnValidationChanged != "" { ent.SetOnValidationChanged(s.GetOnValidationChangedHandler(e.OnValidationChanged)) } if e.Disabled { ent.Disable() } c = ent return } func (w Widget) buildHeaderLabelWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { var rt *widget.RichText level := 1 switch e.Widget { case H1: level = 1 case H2: level = 2 case H3: level = 3 case H4: level = 4 case H5: level = 5 case H6: level = 6 } if e.Binding != "" { rt = uiwidget.NewHWithData(level, s.GetBinding(e.Binding).(binding.String)) } else { rt = uiwidget.NewH(level, e.Text) } for opt, val := range e.Options { switch opt { case "wrapping": rt.Wrapping = getTextWrap(val) case "truncation": rt.Truncation = getTruncation(val) } } c = rt return } func (w Widget) buildIconWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { c = widget.NewIcon(s.GetIcon(e.Icon)) return } func (w Widget) buildLabelWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { var lbl *widget.Label if e.Binding != "" { lbl = widget.NewLabelWithData(s.GetBinding(e.Binding).(binding.String)) } else { lbl = widget.NewLabel(e.Text) } for opt, val := range e.Options { switch opt { case "alignment": lbl.Alignment = getTextAlignment(val) case "wrapping": lbl.Wrapping = getTextWrap(val) case "textStyle": lbl.TextStyle = getTextStyle(val) case "truncation": lbl.Truncation = getTruncation(val) case "importance": lbl.Importance = getImportance(val) } } c = lbl return } func (w Widget) buildListWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { var lst *widget.List if e.Binding != "" { lst = widget.NewListWithData( s.GetBinding(e.Binding).(binding.DataList), s.GetListItemTemplate(e.ItemTemplate), s.GetListDataItemRenderer(e.ItemRenderer)) } else { lst = widget.NewList( s.GetListLength(e.ListLength), s.GetListItemTemplate(e.ItemTemplate), s.GetListItemRenderer(e.ItemRenderer)) } if e.OnSelected != "" { lst.OnSelected = s.GetOnListItemSelectedHandler(e.OnSelected) } if e.OnUnselected != "" { lst.OnUnselected = s.GetOnListItemUnselectedHandler(e.OnUnselected) } c = lst return } func (w Widget) buildSelectWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { var slc *widget.Select var options []string if e.Binding != "" { if lst, ok := s.GetBinding(e.SelectOptionsBinding).(binding.StringList); ok { options, _ = lst.Get() } } else { options = e.SelectOptions } slc = widget.NewSelect(options, s.GetOnOptionSelectedHandler(e.OnOptionSelected)) slc.PlaceHolder = e.Placeholder for opt, val := range e.Options { switch opt { case "alignment": slc.Alignment = getTextAlignment(val) } } if e.Disabled { slc.Disable() } c = slc return } func (w Widget) buildSelectEntryWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { var slc *widget.SelectEntry var options []string if e.SelectOptionsBinding != "" { if lst, ok := s.GetBinding(e.SelectOptionsBinding).(binding.StringList); ok { options, _ = lst.Get() lst.AddListener(binding.NewDataListener(func() { options, _ = lst.Get() if slc != nil { slc.SetOptions(options) } })) } } else { options = e.SelectOptions } slc = widget.NewSelectEntry(options) if e.Binding != "" { slc.Bind(s.GetBinding(e.Binding).(binding.String)) } else { slc.Text = e.Text } slc.PlaceHolder = e.Placeholder if e.Validator != "" { slc.Validator = s.GetValidator(e.Validator) } if e.OnValidationChanged != "" { slc.SetOnValidationChanged(s.GetOnValidationChangedHandler(e.OnValidationChanged)) } if e.Disabled { slc.Disable() } c = slc return } func (w Widget) buildSeparatorWidget( _ *Element, _ ScreenHandler, ) (c fyne.CanvasObject, err error) { sep := widget.NewSeparator() c = sep return } func (w Widget) buildSpacerWidget( e *Element, _ ScreenHandler, ) (c fyne.CanvasObject, err error) { spc := &layout.Spacer{ FixHorizontal: e.FixHorizontal, FixVertical: e.FixVertical, } c = spc return } func (w Widget) buildUpDownLabelWidget( e *Element, s ScreenHandler, ) (c fyne.CanvasObject, err error) { var minSize *fyne.Size if e.MinSize != nil { minSize = &fyne.Size{Width: e.MinSize.Width, Height: e.MinSize.Height} } btn := uiwidget.NewUpDownLabelWithData( minSize, s.GetBinding(e.Binding).(binding.String), s.GetOnClickedHandler(e.OnUpClicked), s.GetOnClickedHandler(e.OnDownClicked)) c = btn return } func getButtonAlignment(v any) widget.ButtonAlign { switch v.(string) { case "ButtonAlignLeading": return widget.ButtonAlignLeading case "ButtonAlignTrailing": return widget.ButtonAlignTrailing } return widget.ButtonAlignCenter } func getButtonIconPlacement(v any) widget.ButtonIconPlacement { if v.(string) == "ButtonIconTrailingText" { return widget.ButtonIconTrailingText } return widget.ButtonIconLeadingText } func getImportance(v any) widget.Importance { switch v.(string) { case "LowImportance": return widget.LowImportance case "HighImportance": return widget.HighImportance case "DangerImportance": return widget.DangerImportance case "WarningImportance": return widget.WarningImportance case "SuccessImportance": return widget.SuccessImportance } return widget.MediumImportance } func getTextAlignment(v any) fyne.TextAlign { switch v.(string) { case "TextAlignCenter": return fyne.TextAlignCenter case "TextAlignTrailing": return fyne.TextAlignTrailing } return fyne.TextAlignLeading } func getTextStyle(v any) fyne.TextStyle { var ts fyne.TextStyle props, ok := v.(map[string]any) if !ok { return ts } for key, val := range props { switch key { case "bold": ts.Bold, _ = val.(bool) case "italic": ts.Italic, _ = val.(bool) case "monospace": ts.Monospace, _ = val.(bool) case "symbol": ts.Symbol, _ = val.(bool) case "tabWidth": ts.TabWidth, _ = val.(int) } } return ts } func getTextWrap(v any) fyne.TextWrap { switch v.(string) { case "TextWrapBreak": return fyne.TextWrapBreak case "TextWrapWord": return fyne.TextWrapWord } return fyne.TextWrapOff } func getTruncation(v any) fyne.TextTruncation { switch v.(string) { case "TextTruncateClip": return fyne.TextTruncateClip case "TextTruncateEllipsis": return fyne.TextTruncateEllipsis } return fyne.TextTruncateOff } func parseTimeFormat(format string) string { switch format { case "ANSIC": return time.ANSIC case "UnixDate": return time.UnixDate case "RubyDate": return time.RubyDate case "RFC822": return time.RFC822 case "RFC822Z": return time.RFC822Z case "RFC850": return time.RFC850 case "RFC1123": return time.RFC1123 case "RFC1123Z": return time.RFC1123Z case "RFC3339": return time.RFC3339 case "RFC3339Nano": return time.RFC3339Nano case "Kitchen": return time.Kitchen case "Stamp": return time.Stamp case "StampMilli": return time.StampMilli case "StampMicro": return time.StampMicro case "StampNano": return time.StampNano case "DateTime": return time.DateTime case "DateOnly": return time.DateOnly case "TimeOnly": return time.TimeOnly default: return format } }