ui/screen/widget.go
2024-08-06 10:31:43 +02:00

825 lines
19 KiB
Go

package screen
import (
"errors"
"fmt"
"log"
"strconv"
"time"
"gitea.hevanto-it.com/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"
RichText Widget = "RichText"
// ScoreDisplay Widget
ScoreDisplay Widget = "ScoreDisplay"
// 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 RichText:
widget, err = w.buildRichTextWidget(e, s)
case ScoreDisplay:
widget, err = w.buildScoreDisplayWidget(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 = applyPadding(e, widget)
decorator = applyDecorators(e, decorator)
decorator = applyMargins(e, 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.ParseInLocation(timeFormat, *e.Time, time.Local); 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) buildRichTextWidget(
e *Element,
s ScreenHandler,
) (c fyne.CanvasObject, err error) {
var rt *widget.RichText
if e.Binding != "" {
data := s.GetBinding(e.Binding).(binding.String)
txt, _ := data.Get()
rt = widget.NewRichTextFromMarkdown(txt)
data.AddListener(binding.NewDataListener(func() {
txt, _ := data.Get()
rt.ParseMarkdown(txt)
}))
} else {
rt = widget.NewRichTextFromMarkdown(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) buildScoreDisplayWidget(
e *Element,
s ScreenHandler,
) (c fyne.CanvasObject, err error) {
var sd *uiwidget.ScoreDisplay
resources := &uiwidget.ScoreDisplayResources{}
for k, v := range e.ResourceSet {
switch k {
case "Empty":
resources.Empty, err = AssetToResource(s, v)
if err != nil {
return
}
default:
var digit int
digit, err = strconv.Atoi(k)
if err != nil {
log.Printf("failed to parse score digit: %v", err)
return
}
if digit > 9 {
continue
}
resources.Digits[digit], err = AssetToResource(s, v)
if err != nil {
return
}
}
}
var relSize, fixedSize *fyne.Size
if e.RelativeSize != nil {
relSize = &fyne.Size{
Width: e.RelativeSize.Width,
Height: e.RelativeSize.Height,
}
}
if e.ElementSize != nil {
fixedSize = &fyne.Size{
Width: e.ElementSize.Width,
Height: e.ElementSize.Height,
}
}
if e.Binding != "" {
sd = uiwidget.NewScoreDisplayWithData(
s.GetBinding(e.Binding).(binding.Int),
e.NumDigits, resources, relSize, fixedSize)
} else {
sd = uiwidget.NewScoreDisplay(
e.Score, e.NumDigits, resources,
relSize, fixedSize)
}
c = sd
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
}
}