package rui import ( "fmt" "strconv" "strings" ) // SizeFunc describes a function that calculates the SizeUnit size. // Used as the value of the SizeUnit properties. // "min", "max", "clamp", "sum", "sub", "mul", and "div" functions are available. type SizeFunc interface { fmt.Stringer // Name() returns the function name: "min", "max", "clamp", "sum", "sub", "mul", or "div" Name() string // Args() returns a list of function arguments Args() []any cssString(session Session) string writeCSS(topFunc string, buffer *strings.Builder, session Session) writeString(topFunc string, buffer *strings.Builder) } type sizeFuncData struct { tag string args []any } func parseSizeFunc(text string) SizeFunc { text = strings.Trim(text, " ") for _, tag := range []string{"min", "max", "sum", "sub", "mul", "div", "clamp"} { if strings.HasPrefix(text, tag) { text = strings.Trim(strings.TrimPrefix(text, tag), " ") last := len(text) - 1 if text[0] == '(' && text[last] == ')' { text = text[1:last] bracket := 0 start := 0 args := []any{} for i, ch := range text { switch ch { case ',': if bracket == 0 { args = append(args, text[start:i]) start = i + 1 } case '(': bracket++ case ')': bracket-- } } if bracket != 0 { ErrorLogF(`Invalid "%s" function`, tag) return nil } args = append(args, text[start:]) switch tag { case "sub", "mul", "div": if len(args) != 2 { ErrorLogF(`"%s" function needs 2 arguments`, tag) return nil } case "clamp": if len(args) != 3 { ErrorLog(`"clamp" function needs 3 arguments`) return nil } } data := new(sizeFuncData) data.tag = tag if data.parseArgs(args, tag == "mul" || tag == "div") { return data } } ErrorLogF(`Invalid "%s" function`, tag) return nil } } return nil } func (data *sizeFuncData) parseArgs(args []any, allowNumber bool) bool { data.args = []any{} numberArg := func(index int, value float64) bool { if allowNumber { if index == 1 { if value == 0 && data.tag == "div" { ErrorLog(`Division by 0 in div function`) return false } data.args = append(data.args, value) return true } else { ErrorLogF(`Only the second %s function argument can be a number`, data.tag) return false } } ErrorLogF(`The %s function argument can't be a number`, data.tag) return false } for i, arg := range args { switch arg := arg.(type) { case string: if arg = strings.Trim(arg, " \t\n"); arg == "" { ErrorLogF(`Unsupported %s function argument #%d: ""`, data.tag, i) return false } if arg[0] == '@' { data.args = append(data.args, arg) } else if val, err := strconv.ParseFloat(arg, 64); err == nil { return numberArg(i, val) } else if fn := parseSizeFunc(arg); fn != nil { data.args = append(data.args, fn) } else if size, err := stringToSizeUnit(arg); err == nil { data.args = append(data.args, size) } else { ErrorLogF(`Unsupported %s function argument #%d: "%s"`, data.tag, i, arg) return false } case SizeFunc: data.args = append(data.args, arg) case SizeUnit: if arg.Type == Auto { ErrorLogF(`Unsupported %s function argument #%d: "auto"`, data.tag, i) } data.args = append(data.args, arg) case float64: return numberArg(i, arg) case float32: return numberArg(i, float64(arg)) default: if n, ok := isInt(arg); ok { return numberArg(i, float64(n)) } ErrorLogF(`Unsupported %s function argument #%d: %v`, data.tag, i, arg) return false } } return true } func (data *sizeFuncData) String() string { buffer := allocStringBuilder() defer freeStringBuilder(buffer) data.writeString("", buffer) return buffer.String() } func (data *sizeFuncData) Name() string { return data.tag } func (data *sizeFuncData) Args() []any { args := make([]any, len(data.args)) copy(args, data.args) return args } func (data *sizeFuncData) writeString(topFunc string, buffer *strings.Builder) { buffer.WriteString(data.tag) buffer.WriteRune('(') for i, arg := range data.args { if i > 0 { buffer.WriteString(", ") } switch arg := arg.(type) { case string: buffer.WriteString(arg) case SizeFunc: arg.writeString(data.tag, buffer) case SizeUnit: buffer.WriteString(arg.String()) case fmt.Stringer: buffer.WriteString(arg.String()) case float64: buffer.WriteString(fmt.Sprintf("%g", arg)) } } buffer.WriteRune(')') } func (data *sizeFuncData) cssString(session Session) string { buffer := allocStringBuilder() defer freeStringBuilder(buffer) data.writeCSS("", buffer, session) return buffer.String() } func (data *sizeFuncData) writeCSS(topFunc string, buffer *strings.Builder, session Session) { bracket := true sep := ", " mathFunc := func(s string) { sep = s switch topFunc { case "": buffer.WriteString("calc(") case "min", "max", "clamp": bracket = false default: buffer.WriteRune('(') } } switch data.tag { case "min", "max", "clamp": buffer.WriteString(data.tag) buffer.WriteRune('(') case "sum": mathFunc(" + ") case "sub": mathFunc(" - ") case "mul": mathFunc(" * ") case "div": mathFunc(" / ") default: return } for i, arg := range data.args { if i > 0 { buffer.WriteString(sep) } switch arg := arg.(type) { case string: if arg, ok := session.resolveConstants(arg); ok { if fn := parseSizeFunc(arg); fn != nil { fn.writeCSS(data.tag, buffer, session) } else if size, err := stringToSizeUnit(arg); err == nil { buffer.WriteString(size.cssString("0", session)) } else { buffer.WriteString("0") } } else { buffer.WriteString("0") } case SizeFunc: arg.writeCSS(data.tag, buffer, session) case SizeUnit: buffer.WriteString(arg.cssString("0", session)) case fmt.Stringer: buffer.WriteString(arg.String()) case float64: buffer.WriteString(fmt.Sprintf("%g", arg)) } } if bracket { buffer.WriteRune(')') } } // MaxSize creates a SizeUnit function that calculates the maximum argument. // Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc func MaxSize(arg0, arg1 any, args ...any) SizeFunc { data := new(sizeFuncData) data.tag = "max" if !data.parseArgs(append([]any{arg0, arg1}, args...), false) { return nil } return data } // MinSize creates a SizeUnit function that calculates the minimum argument. // Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. func MinSize(arg0, arg1 any, args ...any) SizeFunc { data := new(sizeFuncData) data.tag = "min" if !data.parseArgs(append([]any{arg0, arg1}, args...), false) { return nil } return data } // SumSize creates a SizeUnit function that calculates the sum of arguments. // Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. func SumSize(arg0, arg1 any, args ...any) SizeFunc { data := new(sizeFuncData) data.tag = "sum" if !data.parseArgs(append([]any{arg0, arg1}, args...), false) { return nil } return data } // SumSize creates a SizeUnit function that calculates the result of subtracting the arguments (arg1 - arg2). // Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. func SubSize(arg0, arg1 any) SizeFunc { data := new(sizeFuncData) data.tag = "sub" if !data.parseArgs([]any{arg0, arg1}, false) { return nil } return data } // MulSize creates a SizeUnit function that calculates the result of multiplying the arguments (arg1 * arg2). // Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. // The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64) // or a string which is a text representation of a number. func MulSize(arg0, arg1 any) SizeFunc { data := new(sizeFuncData) data.tag = "mul" if !data.parseArgs([]any{arg0, arg1}, true) { return nil } return data } // DivSize creates a SizeUnit function that calculates the result of dividing the arguments (arg1 / arg2). // Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. // The second argument can also be a number (float32, float32, int, int8...int64, uint, uint8...unit64) // or a string which is a text representation of a number. func DivSize(arg0, arg1 any) SizeFunc { data := new(sizeFuncData) data.tag = "div" if !data.parseArgs([]any{arg0, arg1}, true) { return nil } return data } // ClampSize creates a SizeUnit function whose the result is calculated as follows: // // min ≤ value ≤ max -> value; // value < min -> min; // max < value -> max; // // Valid argument types are SizeUnit, SizeFunc and a string which is a text description of SizeUnit or SizeFunc. func ClampSize(min, value, max any) SizeFunc { data := new(sizeFuncData) data.tag = "clamp" if !data.parseArgs([]any{min, value, max}, false) { return nil } return data }