Initial Commit

This commit is contained in:
Maarten Heremans
2024-03-30 17:45:07 +01:00
parent 7112bbbaf3
commit 864255113c
17 changed files with 2747 additions and 0 deletions

106
screen/element.go Normal file
View File

@ -0,0 +1,106 @@
package screen
import (
"fmt"
"bitbucket.org/hevanto/i18n"
"fyne.io/fyne/v2"
)
type Size struct {
Width float32 `yaml:"width"`
Height float32 `yaml:"height"`
}
type Element struct {
ID string `yaml:"id"`
Layout Layout `yaml:"layout"`
Widget Widget `yaml:"widget"`
// Border Layout Elements
Top *Element `yaml:"top"`
Bottom *Element `yaml:"bottom"`
Left *Element `yaml:"left"`
Right *Element `yaml:"right"`
Center []*Element `yaml:"center"`
// Other Layout Elements
Children []*Element `yaml:"children"`
// Form Element
Label *Element `yaml:"label"`
Field *Element `yaml:"field"`
// Grid configuration
Columns *int `yaml:"columns"`
Rows *int `yaml:"rows"`
RowColumns *int `yaml:"rowColumns"`
ElementSize *Size `yaml:"elementSize"`
// Widget Properties
Text string `yaml:"text"`
Localized bool `yaml:"localized"`
Icon string `yaml:"icon"`
Binding string `yaml:"binding"`
Decorators []string `yaml:"decorators"`
Options map[string]any `yaml:"options"`
Disabled bool `yaml:"disabled"`
Hidden bool `yaml:"hidden"`
// Calendar Properties
Time *string `yaml:"time"`
TimeFormat *string `yaml:"timeFormat"`
// List Properties
ItemTemplate string `yaml:"itemTemplate"`
ItemRenderer string `yaml:"itemRenderer"`
ListLength string `yaml:"listLength"`
// Check Properties
Checked bool `yaml:"checked"`
// Spacer Properties
FixHorizontal bool `yaml:"fixHorizontal"`
FixVertical bool `yaml:"fixVertical"`
// Handlers
OnClicked string `yaml:"onClicked"`
OnUpClicked string `yaml:"onUpClicked"`
OnDownClicked string `yaml:"onDownClicked"`
OnChanged string `yaml:"onChanged"`
OnSelected string `yaml:"onSelected"`
OnUnselected string `yaml:"onUnselected"`
OnDateSelected string `yaml:"onDateSelected"`
}
func (e *Element) BuildUI(s ScreenHandler) (obj fyne.CanvasObject, err error) {
defer func() {
if e.ID != "" && obj != nil {
s.RegisterElement(e.ID, obj)
}
}()
if e.Layout != "" {
if obj, err = e.Layout.Build(e, s); err != nil {
err = fmt.Errorf("failed to build layout element: %w", err)
return
}
return
}
if e.Widget != "" {
if obj, err = e.Widget.Build(e, s); err != nil {
err = fmt.Errorf("failed to build widget element: %w", err)
return
}
return
}
return
}
func (e *Element) localize(s ScreenHandler) {
if e.Localized {
e.Text = i18n.T(s.GetLocalizer(), e.Text)
}
}

208
screen/layout.go Normal file
View File

@ -0,0 +1,208 @@
package screen
import (
"errors"
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
)
type Layout string
const (
Border Layout = "Border"
Form Layout = "Form"
Grid Layout = "Grid"
HBox Layout = "HBox"
Stack Layout = "Stack"
VBox Layout = "VBox"
)
func (l Layout) Build(e *Element, s ScreenHandler) (c *fyne.Container, err error) {
switch l {
case Border:
c, err = l.buildBorderLayout(e, s)
case Form:
c, err = l.buildFormLayout(e, s)
case Grid:
c, err = l.buildGridLayout(e, s)
case HBox:
c, err = l.buildHBoxLayout(e, s)
case Stack:
c, err = l.buildStackLayout(e, s)
case VBox:
c, err = l.buildVBoxLayout(e, s)
default:
err = errors.New("invalid layout")
}
if err != nil {
return
}
if e.Hidden {
c.Hide()
}
return
}
func (l Layout) buildBorderLayout(e *Element, s ScreenHandler) (c *fyne.Container, err error) {
var top, left, right, bottom fyne.CanvasObject
var center []fyne.CanvasObject
if e.Top != nil {
if top, err = e.Top.BuildUI(s); err != nil {
err = fmt.Errorf("BorderLayout: failed to create top element: %w", err)
return
}
}
if e.Left != nil {
if left, err = e.Left.BuildUI(s); err != nil {
err = fmt.Errorf("BorderLayout: failed to create left element: %w", err)
return
}
}
if e.Center != nil {
center = make([]fyne.CanvasObject, 0, len(e.Center))
for _, elem := range e.Center {
var ce fyne.CanvasObject
if ce, err = elem.BuildUI(s); err != nil {
err = fmt.Errorf("BorderLayout: failed to create center element: %w", err)
return
}
center = append(center, ce)
}
}
if e.Right != nil {
if right, err = e.Right.BuildUI(s); err != nil {
err = fmt.Errorf("BorderLayout: failed to create right element: %w", err)
return
}
}
if e.Bottom != nil {
if bottom, err = e.Bottom.BuildUI(s); err != nil {
err = fmt.Errorf("BorderLayout: failed to create bottom element: %w", err)
return
}
}
c = container.NewBorder(top, bottom, left, right, center...)
return
}
func (l Layout) buildFormLayout(e *Element, s ScreenHandler) (c *fyne.Container, err error) {
children := make([]fyne.CanvasObject, 0, len(e.Children)*2)
for _, child := range e.Children {
if child.Label == nil {
child.Label = &Element{
Text: "",
}
}
if child.Field == nil {
child.Field = &Element{
Widget: Label,
Text: "",
}
}
child.Label.Widget = Label
if child.Label.Options == nil {
child.Label.Options = make(map[string]any)
}
if child.Label.Options["TextStyle"] == nil {
child.Label.Options["TextStyle"] = make(map[string]any)
}
child.Label.Options["TextStyle"].(map[string]any)["Bold"] = true
var label, field fyne.CanvasObject
if label, err = child.Label.BuildUI(s); err != nil {
err = fmt.Errorf("FormLayout: failed to create label element: %w", err)
return
}
if field, err = child.Field.BuildUI(s); err != nil {
err = fmt.Errorf("FormLayout: failed to create field element: %w", err)
return
}
children = append(children, label, field)
}
c = container.New(layout.NewFormLayout(), children...)
return
}
func (l Layout) buildGridLayout(e *Element, s ScreenHandler) (c *fyne.Container, err error) {
children := make([]fyne.CanvasObject, 0, len(e.Children))
for _, child := range e.Children {
var obj fyne.CanvasObject
if obj, err = child.BuildUI(s); err != nil {
err = fmt.Errorf("GridLayout failed to create child element %w", err)
return
}
children = append(children, obj)
}
if e.Columns != nil {
c = container.NewGridWithColumns(*e.Columns, children...)
return
}
if e.Rows != nil {
c = container.NewGridWithRows(*e.Rows, children...)
return
}
if e.RowColumns != nil {
c = container.NewAdaptiveGrid(*e.RowColumns, children...)
return
}
if e.ElementSize != nil {
c = container.NewGridWrap(fyne.NewSize(e.ElementSize.Width, e.ElementSize.Height), children...)
return
}
err = errors.New("GridLayout failed due to incomplete grid type definition")
return
}
func (l Layout) buildHBoxLayout(e *Element, s ScreenHandler) (c *fyne.Container, err error) {
children := make([]fyne.CanvasObject, 0, len(e.Children))
for _, child := range e.Children {
var obj fyne.CanvasObject
if obj, err = child.BuildUI(s); err != nil {
err = fmt.Errorf("HBoxLayout failed to create child element: %w", err)
return
}
children = append(children, obj)
}
c = container.New(layout.NewHBoxLayout(), children...)
return
}
func (l Layout) buildStackLayout(e *Element, s ScreenHandler) (c *fyne.Container, err error) {
children := make([]fyne.CanvasObject, 0, len(e.Children))
for _, child := range e.Children {
var obj fyne.CanvasObject
if obj, err = child.BuildUI(s); err != nil {
err = fmt.Errorf("StackLayout failed to create child element: %w", err)
return
}
children = append(children, obj)
}
c = container.New(layout.NewStackLayout(), children...)
return
}
func (l Layout) buildVBoxLayout(e *Element, s ScreenHandler) (c *fyne.Container, err error) {
children := make([]fyne.CanvasObject, 0, len(e.Children))
for _, child := range e.Children {
var obj fyne.CanvasObject
if obj, err = child.BuildUI(s); err != nil {
err = fmt.Errorf("VBoxLayout failed to create child element: %w", err)
return
}
children = append(children, obj)
}
c = container.New(layout.NewVBoxLayout(), children...)
return
}

25
screen/listtemplate.go Normal file
View File

@ -0,0 +1,25 @@
package screen
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/widget"
)
type ListItemTemplate struct {
fyne.CanvasObject
*TemplateScreenHandler
}
func (i *ListItemTemplate) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(i.CanvasObject)
}
func NewListItemTemplate(
obj fyne.CanvasObject,
screenHandler *TemplateScreenHandler,
) *ListItemTemplate {
return &ListItemTemplate{
CanvasObject: obj,
TemplateScreenHandler: screenHandler,
}
}

169
screen/screen.go Normal file
View File

@ -0,0 +1,169 @@
package screen
import (
"embed"
"fmt"
"io"
"io/fs"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/nicksnyder/go-i18n/v2/i18n"
"gopkg.in/yaml.v2"
)
type ScreenHandler interface {
RegisterElement(string, fyne.CanvasObject)
GetLocalizer() *i18n.Localizer
GetIcon(string) fyne.Resource
GetBinding(string) binding.DataItem
GetListItemTemplate(string) func() fyne.CanvasObject
GetListDataItemRenderer(string) func(binding.DataItem, fyne.CanvasObject)
GetListItemRenderer(string) func(int, fyne.CanvasObject)
GetListLength(string) func() int
GetOnSelectedHandler(string) func(widget.ListItemID)
GetOnUnselectedHandler(string) func(widget.ListItemID)
GetClickedHandler(string) func()
GetCheckChangedHandler(string) func(bool)
GetDateSelectedHandler(string) func(time.Time)
}
type Screen struct {
rootElement *Element
screenHandler ScreenHandler
}
func New(
filesystem embed.FS,
name string,
screenHandler ScreenHandler,
) (scr *Screen, err error) {
var bytes []byte
var fh fs.File
if fh, err = filesystem.Open(name); err != nil {
err = fmt.Errorf("no screen definition found: %w", err)
return
}
if bytes, err = io.ReadAll(fh); err != nil {
err = fmt.Errorf("unable to read screen definition: %w", err)
return
}
root := &Element{}
if err = yaml.Unmarshal(bytes, root); err != nil {
err = fmt.Errorf("failure parsing screen definition: %w", err)
return
}
scr = &Screen{
rootElement: root,
screenHandler: screenHandler,
}
return
}
func NewTemplate(
filesystem embed.FS,
name string,
localizer *i18n.Localizer,
) (
scr *Screen,
handler *TemplateScreenHandler,
err error,
) {
handler = NewDummyScreenHandler(localizer)
scr, err = New(filesystem, name, handler)
return
}
func (s *Screen) Initialize() (obj fyne.CanvasObject, err error) {
if obj, err = s.rootElement.BuildUI(s.screenHandler); err != nil {
err = fmt.Errorf("failed to build screen: %w", err)
return
}
return
}
type TemplateScreenHandler struct {
localizer *i18n.Localizer
elementsMap map[string]fyne.CanvasObject
}
func NewDummyScreenHandler(localizer *i18n.Localizer) *TemplateScreenHandler {
return &TemplateScreenHandler{
localizer: localizer,
elementsMap: make(map[string]fyne.CanvasObject),
}
}
func (d *TemplateScreenHandler) RegisterElement(name string, element fyne.CanvasObject) {
d.elementsMap[name] = element
}
func (d *TemplateScreenHandler) GetElement(name string) fyne.CanvasObject {
elem, ok := d.elementsMap[name]
if !ok {
return nil
}
return elem
}
func (d *TemplateScreenHandler) GetLocalizer() *i18n.Localizer {
return d.localizer
}
func (d *TemplateScreenHandler) GetIcon(iconName string) fyne.Resource {
if strings.HasPrefix(iconName, "theme.") {
iconName := strings.SplitN(iconName, ".", 2)[1]
return theme.DefaultTheme().Icon(fyne.ThemeIconName(iconName))
}
return nil
}
func (*TemplateScreenHandler) GetBinding(string) binding.DataItem {
return nil
}
func (*TemplateScreenHandler) GetListItemTemplate(string) func() fyne.CanvasObject {
return func() fyne.CanvasObject { return nil }
}
func (*TemplateScreenHandler) GetListDataItemRenderer(string) func(binding.DataItem, fyne.CanvasObject) {
return func(binding.DataItem, fyne.CanvasObject) {}
}
func (*TemplateScreenHandler) GetListItemRenderer(string) func(int, fyne.CanvasObject) {
return func(int, fyne.CanvasObject) {}
}
func (*TemplateScreenHandler) GetListLength(string) func() int {
return func() int { return 0 }
}
func (*TemplateScreenHandler) GetOnSelectedHandler(string) func(widget.ListItemID) {
return func(widget.ListItemID) {}
}
func (*TemplateScreenHandler) GetOnUnselectedHandler(string) func(widget.ListItemID) {
return func(widget.ListItemID) {}
}
func (*TemplateScreenHandler) GetClickedHandler(string) func() {
return func() {}
}
func (*TemplateScreenHandler) GetCheckChangedHandler(string) func(bool) {
return func(bool) {}
}
func (*TemplateScreenHandler) GetDateSelectedHandler(string) func(time.Time) {
return func(time.Time) {}
}

382
screen/widget.go Normal file
View File

@ -0,0 +1,382 @@
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"
)
type Widget string
const (
Button Widget = "Button"
Calendar Widget = "Calendar"
Check Widget = "Check"
H1 Widget = "H1"
H2 Widget = "H2"
H3 Widget = "H3"
H4 Widget = "H4"
H5 Widget = "H5"
H6 Widget = "H6"
Label Widget = "Label"
List Widget = "List"
Separator Widget = "Separator"
Spacer Widget = "Spacer"
UpDownLabel Widget = "UpDownLabel"
)
func (w Widget) Build(e *Element, s ScreenHandler) (c fyne.CanvasObject, err error) {
e.localize(s)
switch w {
case Button:
c, err = w.buildButtonWidget(e, s)
case Calendar:
c, err = w.buildCalendarWidget(e, s)
case Check:
c, err = w.buildCheckWidget(e, s)
case H1, H2, H3, H4, H5, H6:
c, err = w.buildHeaderLabelWidget(e, s)
case Label:
c, err = w.buildLabelWidget(e, s)
case List:
c, err = w.buildListWidget(e, s)
case Separator:
c, err = w.buildSeparatorWidget(e, s)
case Spacer:
c, err = w.buildSpacerWidget(e, s)
case UpDownLabel:
c, err = w.buildUpDownLabelWidget(e, s)
default:
err = errors.New("invalid widget")
}
if err != nil {
return
}
if e.Decorators != nil {
for _, dec := range e.Decorators {
switch dec {
case "Border":
c = uiwidget.NewWidgetBorder(c)
}
}
}
if e.Hidden {
c.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.GetClickedHandler(e.OnClicked))
} else {
btn = widget.NewButton(e.Text, s.GetClickedHandler(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.GetDateSelectedHandler(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.GetCheckChangedHandler(e.OnChanged))
chk.SetChecked(e.Checked)
}
if e.Disabled {
chk.Disable()
}
c = chk
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) 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.GetOnSelectedHandler(e.OnSelected)
}
if e.OnUnselected != "" {
lst.OnUnselected = s.GetOnUnselectedHandler(e.OnUnselected)
}
c = lst
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) {
btn := uiwidget.NewUpDownLabelWithData(
s.GetBinding(e.Binding).(binding.String),
s.GetClickedHandler(e.OnUpClicked),
s.GetClickedHandler(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
}
}